77import java .io .Closeable ;
88import java .io .IOException ;
99import java .io .InputStream ;
10- import java .util .*;
10+ import java .util .ArrayList ;
11+ import java .util .Collections ;
12+ import java .util .Comparator ;
13+ import java .util .List ;
14+ import java .util .Map ;
15+ import java .util .TreeMap ;
1116import java .util .zip .GZIPInputStream ;
1217
1318import javax .annotation .CheckForNull ;
1419import javax .annotation .Nonnull ;
1520
21+ import static java .net .HttpURLConnection .HTTP_OK ;
22+
1623/**
1724 * Response information supplied when a response is received and before the body is processed.
1825 * <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>
1929 * Instances of this class are closed once the response is done being processed. This means that {@link #bodyStream()}
2030 * will not be readable after a call is completed.
2131 *
@@ -35,6 +45,11 @@ public abstract class GitHubConnectorResponse implements Closeable {
3545 private final GitHubConnectorRequest request ;
3646 @ Nonnull
3747 private final Map <String , List <String >> headers ;
48+ private boolean bodyStreamCalled = false ;
49+ private InputStream bodyStream = null ;
50+ private byte [] bodyBytes = null ;
51+ private boolean isClosed = false ;
52+ private boolean isBodyStreamRereadable ;
3853
3954 /**
4055 * GitHubConnectorResponse constructor
@@ -58,6 +73,7 @@ protected GitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
5873 caseInsensitiveMap .put (entry .getKey (), Collections .unmodifiableList (new ArrayList <>(entry .getValue ())));
5974 }
6075 this .headers = Collections .unmodifiableMap (caseInsensitiveMap );
76+ this .isBodyStreamRereadable = false ;
6177 }
6278
6379 /**
@@ -79,17 +95,72 @@ public String header(String name) {
7995 /**
8096 * The response body as an {@link InputStream}.
8197 *
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+ *
82107 * @return the response body
83108 * @throws IOException
84109 * if response stream is null or an I/O Exception occurs.
85110 */
86111 @ Nonnull
87- public abstract InputStream bodyStream () throws IOException ;
112+ public InputStream bodyStream () throws IOException {
113+ synchronized (this ) {
114+ if (isClosed ) {
115+ throw new IOException ("Response is closed" );
116+ }
117+
118+ if (bodyStreamCalled ) {
119+ if (!isBodyStreamRereadable ()) {
120+ throw new IOException ("Response body not rereadable" );
121+ }
122+ } else {
123+ bodyStream = wrapStream (rawBodyStream ());
124+ bodyStreamCalled = true ;
125+ }
126+
127+ if (bodyStream == null ) {
128+ throw new IOException ("Response body missing, stream null" );
129+ } else if (!isBodyStreamRereadable ()) {
130+ return bodyStream ;
131+ }
132+
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+ }
139+
140+ return new ByteArrayInputStream (bodyBytes );
141+ }
142+ }
88143
89144 /**
90- * Gets the {@link GitHubConnectorRequest} for this response.
145+ * Get the raw implementation specific body stream for this response.
146+ *
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.
91149 *
92- * @return the {@link GitHubConnectorRequest} for this response.
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.
152+ *
153+ * @return the stream for the raw response
154+ * @throws IOException
155+ * if an I/O Exception occurs.
156+ */
157+ @ CheckForNull
158+ protected abstract InputStream rawBodyStream () throws IOException ;
159+
160+ /**
161+ * Gets the {@link GitHubConnector} for this response.
162+ *
163+ * @return the {@link GitHubConnector} for this response.
93164 */
94165 @ Nonnull
95166 public GitHubConnectorRequest request () {
@@ -116,6 +187,56 @@ public Map<String, List<String>> allHeaders() {
116187 return headers ;
117188 }
118189
190+ /**
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.
195+ *
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.
200+ */
201+ public boolean isBodyStreamRereadable () {
202+ synchronized (this ) {
203+ return isBodyStreamRereadable || statusCode != HTTP_OK ;
204+ }
205+ }
206+
207+ /**
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.
212+ *
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()}.
218+ */
219+ public void setBodyStreamRereadable () {
220+ synchronized (this ) {
221+ if (bodyStreamCalled && !isBodyStreamRereadable ()) {
222+ throw new RuntimeException ("bodyStream() already called in read-once mode" );
223+ }
224+ isBodyStreamRereadable = true ;
225+ }
226+ }
227+
228+ /**
229+ * {@inheritDoc}
230+ */
231+ @ Override
232+ public void close () throws IOException {
233+ synchronized (this ) {
234+ IOUtils .closeQuietly (bodyStream );
235+ isClosed = true ;
236+ this .bodyBytes = null ;
237+ }
238+ }
239+
119240 /**
120241 * Handles wrapping the body stream if indicated by the "Content-Encoding" header.
121242 *
@@ -155,13 +276,12 @@ public final int parseInt(String name) throws NumberFormatException {
155276
156277 /**
157278 * A ByteArrayResponse class
279+ *
280+ * @deprecated Inherit directly from {@link GitHubConnectorResponse}.
158281 */
282+ @ Deprecated
159283 public abstract static class ByteArrayResponse extends GitHubConnectorResponse {
160284
161- private boolean inputStreamRead = false ;
162- private byte [] inputBytes = null ;
163- private boolean isClosed = false ;
164-
165285 /**
166286 * Constructor for ByteArray Response
167287 *
@@ -177,52 +297,5 @@ protected ByteArrayResponse(@Nonnull GitHubConnectorRequest request,
177297 @ Nonnull Map <String , List <String >> headers ) {
178298 super (request , statusCode , headers );
179299 }
180-
181- /**
182- * {@inheritDoc}
183- */
184- @ Override
185- @ Nonnull
186- public InputStream bodyStream () throws IOException {
187- if (isClosed ) {
188- throw new IOException ("Response is closed" );
189- }
190- synchronized (this ) {
191- if (!inputStreamRead ) {
192- InputStream rawStream = rawBodyStream ();
193- try (InputStream stream = wrapStream (rawStream )) {
194- if (stream != null ) {
195- inputBytes = IOUtils .toByteArray (stream );
196- }
197- }
198- inputStreamRead = true ;
199- }
200- }
201-
202- if (inputBytes == null ) {
203- throw new IOException ("Response body missing, stream null" );
204- }
205-
206- return new ByteArrayInputStream (inputBytes );
207- }
208-
209- /**
210- * Get the raw implementation specific body stream for this response.
211- *
212- * This method will only be called once to completion. If an exception is thrown, it may be called multiple
213- * times.
214- *
215- * @return the stream for the raw response
216- * @throws IOException
217- * if an I/O Exception occurs.
218- */
219- @ CheckForNull
220- protected abstract InputStream rawBodyStream () throws IOException ;
221-
222- @ Override
223- public void close () throws IOException {
224- isClosed = true ;
225- this .inputBytes = null ;
226- }
227300 }
228301}
0 commit comments