|
14 | 14 | import org.springframework.http.client.ClientHttpResponse; |
15 | 15 |
|
16 | 16 | import java.io.ByteArrayInputStream; |
| 17 | +import java.io.ByteArrayOutputStream; |
17 | 18 | import java.io.IOException; |
18 | 19 | import java.io.InputStream; |
19 | 20 |
|
20 | 21 | /** |
21 | 22 | * Spring 6 compatible {@link ClientHttpResponse} implementation that wraps Apache HttpComponents HttpClient 5.x response. |
22 | 23 | * 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 | + * |
23 | 30 | * Package-private as it's only used internally within the common package. |
24 | 31 | */ |
25 | 32 | class HttpClient4ComponentsClientHttpResponse implements ClientHttpResponse { |
26 | 33 |
|
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; |
29 | 38 |
|
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 | + } |
32 | 72 | } |
33 | 73 |
|
34 | 74 | @Override |
35 | 75 | public HttpStatusCode getStatusCode() throws IOException { |
36 | | - return HttpStatusCode.valueOf(httpResponse.getCode()); |
| 76 | + return HttpStatusCode.valueOf(statusCode); |
37 | 77 | } |
38 | 78 |
|
39 | 79 | @Override |
40 | 80 | public int getRawStatusCode() throws IOException { |
41 | | - return httpResponse.getCode(); |
| 81 | + return statusCode; |
42 | 82 | } |
43 | 83 |
|
44 | 84 | @Override |
45 | 85 | public String getStatusText() throws IOException { |
46 | | - return httpResponse.getReasonPhrase(); |
| 86 | + return reasonPhrase; |
47 | 87 | } |
48 | 88 |
|
49 | 89 | @Override |
50 | 90 | 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 | | - } |
57 | 91 | return headers; |
58 | 92 | } |
59 | 93 |
|
60 | 94 | @Override |
61 | 95 | 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); |
64 | 97 | } |
65 | 98 |
|
66 | 99 | @Override |
67 | 100 | 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 |
74 | 102 | } |
75 | 103 | } |
0 commit comments