Skip to content

Commit 89c2efc

Browse files
committed
Cleanup and test coverage
1 parent 7b7f1bd commit 89c2efc

File tree

6 files changed

+289
-82
lines changed

6 files changed

+289
-82
lines changed

src/main/java/org/kohsuke/github/GitHubClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ private void logResponseBody(@Nonnull final GitHubConnectorResponse response) {
629629
LOGGER.log(FINEST, () -> {
630630
String body;
631631
try {
632-
response.forceBufferedBodyStream();
632+
response.setBodyStreamRereadable();
633633
body = GitHubResponse.getBodyAsString(response);
634634
} catch (Throwable e) {
635635
body = "Error reading response body";

src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
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
/**

src/main/java/org/kohsuke/github/extras/HttpClientGitHubConnector.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public GitHubConnectorResponse send(GitHubConnectorRequest connectorRequest) thr
9393
*
9494
* Implementation specific to {@link HttpResponse}.
9595
*/
96-
private static class HttpClientGitHubConnectorResponse extends GitHubConnectorResponse.ByteArrayResponse {
96+
private static class HttpClientGitHubConnectorResponse extends GitHubConnectorResponse {
9797

9898
@Nonnull
9999
private final HttpResponse<InputStream> response;
@@ -113,7 +113,6 @@ protected InputStream rawBodyStream() throws IOException {
113113
@Override
114114
public void close() throws IOException {
115115
super.close();
116-
IOUtils.closeQuietly(response.body());
117116
}
118117
}
119118
}

src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpGitHubConnector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private List<ConnectionSpec> TlsConnectionSpecs() {
104104
*
105105
* Implementation specific to {@link okhttp3.Response}.
106106
*/
107-
private static class OkHttpGitHubConnectorResponse extends GitHubConnectorResponse.ByteArrayResponse {
107+
private static class OkHttpGitHubConnectorResponse extends GitHubConnectorResponse {
108108

109109
@Nonnull
110110
private final Response response;

src/test/java/org/kohsuke/github/RequesterRetryTest.java

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
import okhttp3.ConnectionPool;
44
import okhttp3.OkHttpClient;
55
import org.jetbrains.annotations.NotNull;
6-
import org.jetbrains.annotations.Nullable;
76
import org.junit.Before;
87
import org.junit.Ignore;
98
import org.junit.Test;
109
import org.kohsuke.github.connector.GitHubConnector;
1110
import org.kohsuke.github.connector.GitHubConnectorRequest;
1211
import org.kohsuke.github.connector.GitHubConnectorResponse;
12+
import org.kohsuke.github.connector.GitHubConnectorResponseTest;
1313
import org.kohsuke.github.extras.HttpClientGitHubConnector;
1414
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
1515

1616
import java.io.*;
17-
import java.net.URL;
1817
import java.util.HashMap;
1918
import java.util.List;
2019
import java.util.Map;
@@ -566,55 +565,12 @@ public InputStream bodyStream() throws IOException {
566565
}
567566
}
568567

569-
private static final GitHubConnectorRequest IGNORED_EMPTY_REQUEST = new GitHubConnectorRequest() {
570-
@NotNull
571-
@Override
572-
public String method() {
573-
return null;
574-
}
575-
576-
@NotNull
577-
@Override
578-
public Map<String, List<String>> allHeaders() {
579-
return null;
580-
}
581-
582-
@Nullable
583-
@Override
584-
public String header(String name) {
585-
return null;
586-
}
587-
588-
@Nullable
589-
@Override
590-
public String contentType() {
591-
return null;
592-
}
593-
594-
@Nullable
595-
@Override
596-
public InputStream body() {
597-
return null;
598-
}
599-
600-
@NotNull
601-
@Override
602-
public URL url() {
603-
return null;
604-
}
605-
606-
@Override
607-
public boolean hasBody() {
608-
return false;
609-
}
610-
};
611-
612568
private static class GitHubConnectorResponseWrapper extends GitHubConnectorResponse {
613569

614570
private final GitHubConnectorResponse wrapped;
615571

616572
GitHubConnectorResponseWrapper(GitHubConnectorResponse response) {
617-
super(IGNORED_EMPTY_REQUEST, -1, new HashMap<>());
573+
super(GitHubConnectorResponseTest.EMPTY_REQUEST, -1, new HashMap<>());
618574
wrapped = response;
619575
}
620576

0 commit comments

Comments
 (0)