2323/**
2424 * Response information supplied when a response is received and before the body is processed.
2525 * <p>
26+ * During a request to GitHub, {@link GitHubConnector#send(GitHubConnectorRequest)} returns a
27+ * {@link GitHubConnectorResponse}. This is processed to create a GitHubResponse.
28+ * <p>
2629 * Instances of this class are closed once the response is done being processed. This means that {@link #bodyStream()}
2730 * will not be readable after a call is completed.
2831 *
@@ -43,11 +46,10 @@ public abstract class GitHubConnectorResponse implements Closeable {
4346 @ Nonnull
4447 private final Map <String , List <String >> headers ;
4548 private boolean bodyStreamCalled = false ;
46- private boolean bodyBytesLoaded = false ;
4749 private InputStream bodyStream = null ;
4850 private byte [] bodyBytes = null ;
4951 private boolean isClosed = false ;
50- private boolean forceBufferedBodyStream ;
52+ private boolean isBodyStreamRereadable ;
5153
5254 /**
5355 * GitHubConnectorResponse constructor
@@ -71,6 +73,7 @@ protected GitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
7173 caseInsensitiveMap .put (entry .getKey (), Collections .unmodifiableList (new ArrayList <>(entry .getValue ())));
7274 }
7375 this .headers = Collections .unmodifiableMap (caseInsensitiveMap );
76+ this .isBodyStreamRereadable = false ;
7477 }
7578
7679 /**
@@ -92,53 +95,60 @@ public String header(String name) {
9295 /**
9396 * The response body as an {@link InputStream}.
9497 *
98+ * When {@link #isBodyStreamRereadable} is false, {@link #bodyStream()} can only be called once and the returned
99+ * stream should be assumed to be read-once and not resetable. This is the default behavior for HTTP_OK responses
100+ * and significantly reduces memory usage.
101+ *
102+ * When {@link #isBodyStreamRereadable} is true, {@link #bodyStream()} can be called be called multiple times. The
103+ * full stream data is read into a byte array during the first call. Each call returns a new stream backed by the
104+ * same byte array. This uses more memory, but is required to enable rereading the body stream during trace logging,
105+ * debugging, and error responses.
106+ *
95107 * @return the response body
96108 * @throws IOException
97109 * if response stream is null or an I/O Exception occurs.
98110 */
99111 @ Nonnull
100112 public InputStream bodyStream () throws IOException {
101- InputStream body = null ;
102113 synchronized (this ) {
103114 if (isClosed ) {
104115 throw new IOException ("Response is closed" );
105116 }
106117
107118 if (bodyStreamCalled ) {
108- if (!bodyBytesLoaded ) {
109- throw new IOException ("Response is already consumed " );
119+ if (!isBodyStreamRereadable () ) {
120+ throw new IOException ("Response body not rereadable " );
110121 }
111122 } else {
112- body = wrapStream (rawBodyStream ());
123+ bodyStream = wrapStream (rawBodyStream ());
113124 bodyStreamCalled = true ;
114- bodyStream = body ;
115- if (useBufferedBodyStream ()) {
116- bodyBytesLoaded = true ;
117- try (InputStream stream = body ) {
118- if (stream != null ) {
119- bodyBytes = IOUtils .toByteArray (stream );
120- }
121- }
122- bodyStream = null ;
123- }
124125 }
125126
126- if (bodyBytesLoaded ) {
127- body = bodyBytes == null ? null : new ByteArrayInputStream (bodyBytes );
127+ if (bodyStream == null ) {
128+ throw new IOException ("Response body missing, stream null" );
129+ } else if (!isBodyStreamRereadable ()) {
130+ return bodyStream ;
128131 }
129- }
130132
131- if (body == null ) {
132- throw new IOException ("Response body missing, stream null" );
133- }
133+ // Load rereadable byte array
134+ if (bodyBytes == null ) {
135+ bodyBytes = IOUtils .toByteArray (bodyStream );
136+ // Close the raw body stream after successfully reading
137+ IOUtils .closeQuietly (bodyStream );
138+ }
134139
135- return body ;
140+ return new ByteArrayInputStream (bodyBytes );
141+ }
136142 }
137143
138144 /**
139145 * Get the raw implementation specific body stream for this response.
140146 *
141- * This method will only be called once to completion. If an exception is thrown, it may be called multiple times.
147+ * This method will only be called once to completion. If an exception is thrown by this method, it may be called
148+ * multiple times.
149+ *
150+ * The stream returned from this method will be closed when the response is closed or sooner. Inheriting classes do
151+ * not need to close it.
142152 *
143153 * @return the stream for the raw response
144154 * @throws IOException
@@ -178,24 +188,40 @@ public Map<String, List<String>> allHeaders() {
178188 }
179189
180190 /**
181- * Use unbufferred body stream.
191+ * The body stream rereadable state.
192+ *
193+ * Body stream defaults to read once for HTTP_OK responses (to reduce memory usage). For non-HTTP_OK responses, body
194+ * stream is switched to rereadable (in-memory byte array) for error processing.
182195 *
183- * @return true when unbuffered body stream can should be used.
196+ * Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
197+ * response regardless of {@link #statusCode} value.
198+ *
199+ * @return true when body stream is rereadable.
184200 */
185- boolean useBufferedBodyStream () {
201+ public boolean isBodyStreamRereadable () {
186202 synchronized (this ) {
187- return forceBufferedBodyStream || statusCode () != HTTP_OK ;
203+ return isBodyStreamRereadable || statusCode != HTTP_OK ;
188204 }
189205 }
190206
191207 /**
192- * Use unbufferred body stream.
208+ * Force body stream to rereadable regardless of status code.
209+ *
210+ * Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
211+ * response regardless of {@link #statusCode} value.
193212 *
194- * @return true when unbuffered body stream can should be used.
213+ * This is required to support body value logging during low-level tracing but should be avoided in general since it
214+ * consumes significantly more memory.
215+ *
216+ * Will throw runtime exception if a non-rereadable body stream has already been returned from
217+ * {@link #bodyStream()}.
195218 */
196- public void forceBufferedBodyStream () {
219+ public void setBodyStreamRereadable () {
197220 synchronized (this ) {
198- this .forceBufferedBodyStream = true ;
221+ if (bodyStreamCalled && !isBodyStreamRereadable ()) {
222+ throw new RuntimeException ("bodyStream() already called in read-once mode" );
223+ }
224+ isBodyStreamRereadable = true ;
199225 }
200226 }
201227
@@ -250,7 +276,10 @@ public final int parseInt(String name) throws NumberFormatException {
250276
251277 /**
252278 * A ByteArrayResponse class
279+ *
280+ * @deprecated Inherit directly from {@link GitHubConnectorResponse}.
253281 */
282+ @ Deprecated
254283 public abstract static class ByteArrayResponse extends GitHubConnectorResponse {
255284
256285 /**
0 commit comments