Skip to content

Commit 9d99417

Browse files
authored
fix: (OData) HttpResponse entity buffering (#839)
1 parent 69d5813 commit 9d99417

File tree

7 files changed

+78
-26
lines changed

7 files changed

+78
-26
lines changed

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHealthyResponseValidator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ class ODataHealthyResponseValidator
3838
static void requireHealthyResponse( @Nonnull final ODataRequestResult result )
3939
{
4040
final ODataRequestGeneric originalRequest = result.getODataRequest();
41-
final HttpResponse httpResponse = result.getHttpResponse();
42-
final StatusLine statusLine = httpResponse.getStatusLine();
41+
final StatusLine statusLine = result.getStatusLine();
4342

4443
if( statusLine != null && statusLine.getStatusCode() < HttpStatus.SC_BAD_REQUEST ) { // code < 400
4544
return;
4645
}
4746

47+
final HttpResponse httpResponse = result.getHttpResponse();
4848
final ODataRequestGeneric requestRelevantForException =
4949
findPotentialBatchItem(httpResponse, originalRequest).getOrElse(originalRequest);
5050

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResult.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import java.util.TreeMap;
88

99
import javax.annotation.Nonnull;
10+
import javax.annotation.Nullable;
1011

1112
import org.apache.http.Header;
1213
import org.apache.http.HttpResponse;
14+
import org.apache.http.StatusLine;
1315

1416
/**
1517
* Generic type of an OData request result.
@@ -32,6 +34,17 @@ public interface ODataRequestResult
3234
@Nonnull
3335
HttpResponse getHttpResponse();
3436

37+
/**
38+
* Get the HTTP response object status line.
39+
*
40+
* @return the StatusLine.
41+
*/
42+
@Nullable
43+
default StatusLine getStatusLine()
44+
{
45+
return getHttpResponse().getStatusLine();
46+
}
47+
3548
/**
3649
* Get the iterable list of HTTP response header names.
3750
*

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ public ODataRequestResultGeneric(
126126
deserializer = new ODataResponseDeserializer(protocol);
127127
}
128128

129+
@Nullable
130+
@Override
131+
public StatusLine getStatusLine()
132+
{
133+
return httpResponse.getStatusLine();
134+
}
135+
129136
/**
130137
* Set the default number deserialization strategy for generic JSON numbers without target type mapping.
131138
*

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultMultipartGeneric.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.apache.http.HttpEntity;
1010
import org.apache.http.HttpResponse;
11+
import org.apache.http.StatusLine;
1112
import org.apache.http.util.EntityUtils;
1213

1314
import com.sap.cloud.sdk.datamodel.odata.client.exception.ODataDeserializationException;
@@ -61,6 +62,13 @@ public class ODataRequestResultMultipartGeneric
6162
this.httpResponse = httpResponse;
6263
}
6364

65+
@Nullable
66+
@Override
67+
public StatusLine getStatusLine()
68+
{
69+
return httpResponse.getStatusLine();
70+
}
71+
6472
/**
6573
* Get the original {@link ODataRequestBatch batch request} that was used for running the OData request.
6674
*

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHealthyResponseValidatorTest.java

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
package com.sap.cloud.sdk.datamodel.odata.client.request;
22

3+
import static org.apache.http.HttpVersion.HTTP_1_1;
34
import static org.assertj.core.api.Assertions.assertThat;
45
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5-
import static org.mockito.Mockito.lenient;
6-
import static org.mockito.Mockito.mock;
7-
import static org.mockito.Mockito.when;
86

97
import java.nio.charset.StandardCharsets;
108

11-
import org.apache.http.Header;
129
import org.apache.http.HttpResponse;
1310
import org.apache.http.HttpStatus;
14-
import org.apache.http.StatusLine;
1511
import org.apache.http.entity.StringEntity;
16-
import org.junit.jupiter.api.BeforeEach;
12+
import org.apache.http.message.BasicHttpResponse;
1713
import org.junit.jupiter.api.Test;
1814

1915
import com.sap.cloud.sdk.datamodel.odata.client.ODataProtocol;
@@ -22,32 +18,23 @@
2218

2319
class ODataHealthyResponseValidatorTest
2420
{
25-
private final ODataRequestResult odataResult = mock(ODataRequestResult.class);
26-
private final ODataRequestGeneric odataRequest = mock(ODataRequestGeneric.class);
27-
private final HttpResponse httpResponse = mock(HttpResponse.class);
28-
private final StatusLine httpResponseStatusLine = mock(StatusLine.class);
29-
30-
@BeforeEach
31-
void adjustMocks()
32-
{
33-
lenient().when(odataRequest.getProtocol()).thenReturn(ODataProtocol.V2);
34-
lenient().when(odataResult.getHttpResponse()).thenReturn(httpResponse);
35-
lenient().when(odataResult.getODataRequest()).thenReturn(odataRequest);
36-
lenient().when(httpResponse.getStatusLine()).thenReturn(httpResponseStatusLine);
37-
lenient().when(httpResponse.getAllHeaders()).thenReturn(new Header[0]);
38-
lenient().when(httpResponseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK);
39-
}
21+
private static final ODataRequestGeneric REQUEST =
22+
new ODataRequestRead("service-path", "EntitySet", null, ODataProtocol.V2);
4023

4124
@Test
4225
void testSuccess()
4326
{
27+
final HttpResponse httpResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
28+
final ODataRequestResult odataResult = new ODataRequestResultGeneric(REQUEST, httpResponse);
29+
4430
ODataHealthyResponseValidator.requireHealthyResponse(odataResult);
4531
}
4632

4733
@Test
4834
void testNotFound()
4935
{
50-
when(httpResponseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_NOT_FOUND);
36+
final HttpResponse httpResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_FOUND, "Not Found");
37+
final ODataRequestResult odataResult = new ODataRequestResultGeneric(REQUEST, httpResponse);
5138

5239
assertThatExceptionOfType(ODataResponseException.class)
5340
.isThrownBy(() -> ODataHealthyResponseValidator.requireHealthyResponse(odataResult))
@@ -78,8 +65,9 @@ void testODataError()
7865
}
7966
""";
8067

81-
when(httpResponseStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR);
82-
when(httpResponse.getEntity()).thenReturn(new StringEntity(odata_error_json, StandardCharsets.UTF_8));
68+
final HttpResponse httpResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_INTERNAL_SERVER_ERROR, "Oh!");
69+
httpResponse.setEntity(new StringEntity(odata_error_json, StandardCharsets.UTF_8));
70+
final ODataRequestResult odataResult = new ODataRequestResultGeneric(REQUEST, httpResponse);
8371

8472
assertThatExceptionOfType(ODataServiceErrorException.class)
8573
.isThrownBy(() -> ODataHealthyResponseValidator.requireHealthyResponse(odataResult))

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGenericTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import static org.mockito.Mockito.mock;
1212
import static org.mockito.Mockito.when;
1313

14+
import java.io.ByteArrayInputStream;
15+
import java.io.InputStream;
1416
import java.net.SocketException;
17+
import java.nio.charset.StandardCharsets;
1518
import java.util.Collection;
1619
import java.util.Collections;
1720
import java.util.Map;
@@ -20,6 +23,7 @@
2023
import org.apache.http.HttpEntity;
2124
import org.apache.http.HttpResponse;
2225
import org.apache.http.client.HttpClient;
26+
import org.apache.http.entity.InputStreamEntity;
2327
import org.apache.http.entity.StringEntity;
2428
import org.apache.http.message.BasicHeader;
2529
import org.apache.http.message.BasicHttpResponse;
@@ -162,4 +166,35 @@ void ensureNoRedundantHeadersForPaginatedRequests()
162166
final Map<String, Collection<String>> lastRequestHeaders = nextResult.getODataRequest().getHeaders();
163167
assertThat(lastRequestHeaders).containsExactly(entry("Accept", Collections.singletonList("application/json")));
164168
}
169+
170+
@Test
171+
@SneakyThrows
172+
void testDisabledBuffer()
173+
{
174+
// test setup for request
175+
final ODataRequestGeneric oDataRequest =
176+
new ODataRequestRead("generic/service/path", "entity(123)", null, ODataProtocol.V4);
177+
178+
// test setup for streamed http response
179+
final BasicHttpResponse httpResponse = new BasicHttpResponse(HTTP_1_1, 200, "OK");
180+
final String json = "{\"value\":[]}";
181+
final InputStream inputStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
182+
httpResponse.setEntity(new InputStreamEntity(inputStream, json.length()));
183+
184+
// system under test
185+
final ODataRequestResultGeneric testResult = new ODataRequestResultGeneric(oDataRequest, httpResponse);
186+
testResult.disableBufferingHttpResponse();
187+
188+
// sanity checks do not consume the response
189+
assertThat(testResult.getHeaderValues("Content-Length")).isEmpty();
190+
assertThat(testResult.getHttpResponse().getStatusLine().getStatusCode()).isEqualTo(200);
191+
192+
// true-positive, successfully read once
193+
assertThat(testResult.asListOfMaps()).isEmpty();
194+
195+
// true-negative, no second read possible
196+
assertThatThrownBy(testResult::asListOfMaps)
197+
.isInstanceOf(ODataDeserializationException.class)
198+
.hasMessageContaining("Unable to read OData 4.0 response.");
199+
}
165200
}

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@
2121
### 🐛 Fixed Issues
2222

2323
- Fix `CVE-2025-48734` by transitive dependency update in `connectivity-ztis`.
24+
- For OData Generic Client: Fix `disableBufferingHttpResponse()` in `ODataRequestResultGeneric`.

0 commit comments

Comments
 (0)