Skip to content

Commit 25dc4dc

Browse files
committed
JIRA:GRIF-315 upgrade to httpclient5 full
1 parent 550b6b3 commit 25dc4dc

File tree

8 files changed

+114
-318
lines changed

8 files changed

+114
-318
lines changed

gooddata-java/src/main/java/com/gooddata/sdk/common/HttpClient4ComponentsClientHttpRequestFactory.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
import org.apache.hc.core5.http.ClassicHttpRequest;
10+
import org.apache.hc.core5.http.ClassicHttpResponse;
1011
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
1112
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
1213
import org.apache.hc.client5.http.classic.HttpClient;
@@ -139,12 +140,27 @@ public ClientHttpResponse execute() throws IOException {
139140
// Set headers exactly like reference implementation
140141
addHeaders(httpRequest);
141142

142-
// Execute the request using HttpClient 5.x API
143-
// The execute method with ResponseHandler automatically handles the response
144-
return httpClient.execute(httpRequest, response -> {
145-
// We need to consume and store the response since ResponseHandler closes it
143+
// Extract HttpHost from the request URI for GoodDataHttpClient
144+
// GoodDataHttpClient requires the target host to be explicitly provided
145+
// to properly handle authentication and token management
146+
try {
147+
URI requestUri = httpRequest.getUri();
148+
org.apache.hc.core5.http.HttpHost target = new org.apache.hc.core5.http.HttpHost(
149+
requestUri.getScheme(),
150+
requestUri.getHost(),
151+
requestUri.getPort()
152+
);
153+
154+
// CRITICAL: Call execute() WITHOUT ResponseHandler to ensure GoodDataHttpClient's
155+
// authentication logic is invoked. The version with ResponseHandler bypasses auth!
156+
// See: gooddata-http-client:2.0.0 GoodDataHttpClient.execute() implementation
157+
ClassicHttpResponse response = httpClient.execute(target, httpRequest);
158+
159+
// We need to consume and store the response immediately since the connection may be closed
146160
return new HttpClient4ComponentsClientHttpResponse(response);
147-
});
161+
} catch (java.net.URISyntaxException e) {
162+
throw new IOException("Failed to extract target host from request URI", e);
163+
}
148164
}
149165

150166
/**

gooddata-java/src/main/java/com/gooddata/sdk/common/HttpClient4ComponentsClientHttpResponse.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,62 +14,90 @@
1414
import org.springframework.http.client.ClientHttpResponse;
1515

1616
import java.io.ByteArrayInputStream;
17+
import java.io.ByteArrayOutputStream;
1718
import java.io.IOException;
1819
import java.io.InputStream;
1920

2021
/**
2122
* Spring 6 compatible {@link ClientHttpResponse} implementation that wraps Apache HttpComponents HttpClient 5.x response.
2223
* This bridges HttpClient 5.x responses with Spring 6's ClientHttpResponse interface.
24+
*
25+
* <p>IMPORTANT: This class buffers the entire response body in memory to avoid issues with
26+
* HttpClient 5.x's ResponseHandler automatically closing the response stream. This is necessary
27+
* because Spring 6's IntrospectingClientHttpResponse needs to read the stream after the
28+
* ResponseHandler has returned.
29+
*
2330
* Package-private as it's only used internally within the common package.
2431
*/
2532
class HttpClient4ComponentsClientHttpResponse implements ClientHttpResponse {
2633

27-
private final ClassicHttpResponse httpResponse;
28-
private HttpHeaders headers;
34+
private final int statusCode;
35+
private final String reasonPhrase;
36+
private final HttpHeaders headers;
37+
private final byte[] bodyBytes;
2938

30-
public HttpClient4ComponentsClientHttpResponse(ClassicHttpResponse httpResponse) {
31-
this.httpResponse = httpResponse;
39+
/**
40+
* Creates a response wrapper that immediately buffers the response body.
41+
* This must be called within the ResponseHandler before the response is closed.
42+
*
43+
* @param httpResponse the HttpClient 5.x response to wrap
44+
* @throws IOException if reading the response body fails
45+
*/
46+
public HttpClient4ComponentsClientHttpResponse(ClassicHttpResponse httpResponse) throws IOException {
47+
// Capture response metadata immediately
48+
this.statusCode = httpResponse.getCode();
49+
this.reasonPhrase = httpResponse.getReasonPhrase();
50+
51+
// Copy headers
52+
this.headers = new HttpHeaders();
53+
for (Header header : httpResponse.getHeaders()) {
54+
this.headers.add(header.getName(), header.getValue());
55+
}
56+
57+
// Buffer the response body BEFORE the ResponseHandler closes the stream
58+
HttpEntity entity = httpResponse.getEntity();
59+
if (entity != null) {
60+
try (InputStream content = entity.getContent()) {
61+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
62+
byte[] chunk = new byte[8192];
63+
int bytesRead;
64+
while ((bytesRead = content.read(chunk)) != -1) {
65+
buffer.write(chunk, 0, bytesRead);
66+
}
67+
this.bodyBytes = buffer.toByteArray();
68+
}
69+
} else {
70+
this.bodyBytes = new byte[0];
71+
}
3272
}
3373

3474
@Override
3575
public HttpStatusCode getStatusCode() throws IOException {
36-
return HttpStatusCode.valueOf(httpResponse.getCode());
76+
return HttpStatusCode.valueOf(statusCode);
3777
}
3878

3979
@Override
4080
public int getRawStatusCode() throws IOException {
41-
return httpResponse.getCode();
81+
return statusCode;
4282
}
4383

4484
@Override
4585
public String getStatusText() throws IOException {
46-
return httpResponse.getReasonPhrase();
86+
return reasonPhrase;
4787
}
4888

4989
@Override
5090
public HttpHeaders getHeaders() {
51-
if (headers == null) {
52-
headers = new HttpHeaders();
53-
for (Header header : httpResponse.getHeaders()) {
54-
headers.add(header.getName(), header.getValue());
55-
}
56-
}
5791
return headers;
5892
}
5993

6094
@Override
6195
public InputStream getBody() throws IOException {
62-
HttpEntity entity = httpResponse.getEntity();
63-
return (entity != null) ? entity.getContent() : new ByteArrayInputStream(new byte[0]);
96+
return new ByteArrayInputStream(bodyBytes);
6497
}
6598

6699
@Override
67100
public void close() {
68-
// HttpClient 5.x - close the response
69-
try {
70-
httpResponse.close();
71-
} catch (IOException e) {
72-
// Ignore close exceptions
73-
}
101+
// Nothing to close - response was already consumed and buffered
74102
}
75103
}

gooddata-java/src/main/java/com/gooddata/sdk/common/UriPrefixingClientHttpRequestFactory.java

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,46 +64,54 @@ private URI createUri(URI uri) {
6464
}
6565

6666
// Build complete URI with host information from baseUri
67-
UriComponentsBuilder builder = UriComponentsBuilder.newInstance()
68-
.scheme(baseUri.getScheme())
69-
.host(baseUri.getHost())
70-
.port(baseUri.getPort());
67+
// For queries with edge-case encodings (%80, %FF), we need to avoid UriComponentsBuilder
68+
// validation and construct the URI string manually to preserve invalid UTF-8 sequences
69+
70+
String scheme = baseUri.getScheme();
71+
String host = baseUri.getHost();
72+
int port = baseUri.getPort();
7173

7274
// Handle path - combine base path with request path
7375
String basePath = baseUri.getPath();
7476
String requestPath = uri.getPath();
77+
String finalPath;
7578

7679
if (requestPath != null) {
7780
if (requestPath.startsWith("/")) {
7881
// Absolute path - use as-is
79-
builder.path(requestPath);
82+
finalPath = requestPath;
8083
} else {
8184
// Relative path - append to base path
82-
String combinedPath = (basePath != null && !basePath.endsWith("/")) ? basePath + "/" + requestPath : requestPath;
83-
builder.path(combinedPath);
85+
finalPath = (basePath != null && !basePath.endsWith("/")) ? basePath + "/" + requestPath : requestPath;
8486
}
8587
} else {
86-
builder.path(basePath);
87-
}
88-
89-
// Add query and fragment if present
90-
if (uri.getQuery() != null) {
91-
builder.query(uri.getQuery());
92-
}
93-
94-
if (uri.getFragment() != null) {
95-
builder.fragment(uri.getFragment());
88+
finalPath = basePath;
9689
}
9790

98-
URI result = builder.build().toUri();
99-
100-
// Ensure the result has host information - this is critical!
101-
if (result.getHost() == null) {
102-
throw new IllegalStateException("Generated URI missing host information: " + result +
103-
" (baseUri: " + baseUri + ", requestUri: " + uri + ")");
91+
try {
92+
// Use multi-argument URI constructor to preserve exact encoding
93+
// This avoids validation and re-encoding of the query string
94+
URI result = new URI(
95+
scheme,
96+
null, // userInfo
97+
host,
98+
port,
99+
finalPath,
100+
uri.getQuery(),
101+
uri.getFragment()
102+
);
103+
104+
// Ensure the result has host information - this is critical!
105+
if (result.getHost() == null) {
106+
throw new IllegalStateException("Generated URI missing host information: " + result +
107+
" (baseUri: " + baseUri + ", requestUri: " + uri + ")");
108+
}
109+
110+
return result;
111+
} catch (Exception e) {
112+
throw new IllegalStateException("Failed to create URI (scheme=" + scheme + ", host=" + host +
113+
", port=" + port + ", path=" + finalPath + ", query=" + uri.getQuery() + ")", e);
104114
}
105-
106-
return result;
107115
}
108116

109117
/**

gooddata-java/src/main/java/com/gooddata/sdk/service/httpcomponents/HttpClient4ClientHttpRequest.java

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)