GitHubHttpUrlConnectionClient.java
package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.authorization.AuthorizationProvider;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import javax.annotation.Nonnull;
import static java.util.logging.Level.*;
import static org.apache.commons.lang3.StringUtils.defaultString;
/**
* A GitHub API Client for HttpUrlConnection
* <p>
* A GitHubClient can be used to send requests and retrieve their responses. GitHubClient is thread-safe and can be used
* to send multiple requests. GitHubClient also track some GitHub API information such as {@link GHRateLimit}.
* </p>
* <p>
* GitHubHttpUrlConnectionClient gets a new {@link HttpURLConnection} for each call to send.
* </p>
*/
class GitHubHttpUrlConnectionClient extends GitHubClient {
GitHubHttpUrlConnectionClient(String apiUrl,
HttpConnector connector,
RateLimitHandler rateLimitHandler,
AbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker,
Consumer<GHMyself> myselfConsumer,
AuthorizationProvider authorizationProvider) throws IOException {
super(apiUrl,
connector,
rateLimitHandler,
abuseLimitHandler,
rateLimitChecker,
myselfConsumer,
authorizationProvider);
}
@Nonnull
protected GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException {
HttpURLConnection connection;
try {
connection = HttpURLConnectionResponseInfo.setupConnection(this, request);
} catch (IOException e) {
// An error in here should be wrapped to bypass http exception wrapping.
throw new GHIOException(e.getMessage(), e);
}
// HttpUrlConnection is nuts. This call opens the connection and gets a response.
// Putting this on it's own line for ease of debugging if needed.
int statusCode = connection.getResponseCode();
Map<String, List<String>> headers = connection.getHeaderFields();
return new HttpURLConnectionResponseInfo(request, statusCode, headers, connection);
}
protected void handleLimitingErrors(@Nonnull GitHubResponse.ResponseInfo responseInfo) throws IOException {
if (isRateLimitResponse(responseInfo)) {
GHIOException e = new HttpException("Rate limit violation",
responseInfo.statusCode(),
responseInfo.headerField("Status"),
responseInfo.url().toString()).withResponseHeaderFields(responseInfo.headers());
rateLimitHandler.onError(e, ((HttpURLConnectionResponseInfo) responseInfo).connection);
} else if (isAbuseLimitResponse(responseInfo)) {
GHIOException e = new HttpException("Abuse limit violation",
responseInfo.statusCode(),
responseInfo.headerField("Status"),
responseInfo.url().toString()).withResponseHeaderFields(responseInfo.headers());
abuseLimitHandler.onError(e, ((HttpURLConnectionResponseInfo) responseInfo).connection);
}
}
/**
* Initial response information supplied to a {@link GitHubResponse.BodyHandler} when a response is initially
* received and before the body is processed.
*
* Implementation specific to {@link HttpURLConnection}.
*/
static class HttpURLConnectionResponseInfo extends GitHubResponse.ResponseInfo {
@Nonnull
private final HttpURLConnection connection;
HttpURLConnectionResponseInfo(@Nonnull GitHubRequest request,
int statusCode,
@Nonnull Map<String, List<String>> headers,
@Nonnull HttpURLConnection connection) {
super(request, statusCode, headers);
this.connection = connection;
}
@Nonnull
static HttpURLConnection setupConnection(@Nonnull GitHubClient client, @Nonnull GitHubRequest request)
throws IOException {
HttpURLConnection connection = client.getConnector().connect(request.url());
// if the authentication is needed but no credential is given, try it anyway (so that some calls
// that do work with anonymous access in the reduced form should still work.)
if (!request.headers().containsKey("Authorization")) {
String authorization = client.getEncodedAuthorization();
if (authorization != null) {
connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
}
}
setRequestMethod(request.method(), connection);
buildRequest(request, connection);
return connection;
}
/**
* Set up the request parameters or POST payload.
*/
private static void buildRequest(GitHubRequest request, HttpURLConnection connection) throws IOException {
for (Map.Entry<String, String> e : request.headers().entrySet()) {
String v = e.getValue();
if (v != null)
connection.setRequestProperty(e.getKey(), v);
}
connection.setRequestProperty("Accept-Encoding", "gzip");
if (request.inBody()) {
connection.setDoOutput(true);
try (InputStream body = request.body()) {
if (body != null) {
connection.setRequestProperty("Content-type",
defaultString(request.contentType(), "application/x-www-form-urlencoded"));
byte[] bytes = new byte[32768];
int read;
while ((read = body.read(bytes)) != -1) {
connection.getOutputStream().write(bytes, 0, read);
}
} else {
connection.setRequestProperty("Content-type",
defaultString(request.contentType(), "application/json"));
Map<String, Object> json = new HashMap<>();
for (GitHubRequest.Entry e : request.args()) {
json.put(e.key, e.value);
}
getMappingObjectWriter().writeValue(connection.getOutputStream(), json);
}
}
}
}
private static void setRequestMethod(String method, HttpURLConnection connection) throws IOException {
try {
connection.setRequestMethod(method);
} catch (ProtocolException e) {
// JDK only allows one of the fixed set of verbs. Try to override that
try {
Field $method = HttpURLConnection.class.getDeclaredField("method");
$method.setAccessible(true);
$method.set(connection, method);
} catch (Exception x) {
throw (IOException) new IOException("Failed to set the custom verb").initCause(x);
}
// sun.net.www.protocol.https.DelegatingHttpsURLConnection delegates to another HttpURLConnection
try {
Field $delegate = connection.getClass().getDeclaredField("delegate");
$delegate.setAccessible(true);
Object delegate = $delegate.get(connection);
if (delegate instanceof HttpURLConnection) {
HttpURLConnection nested = (HttpURLConnection) delegate;
setRequestMethod(method, nested);
}
} catch (NoSuchFieldException x) {
// no problem
} catch (IllegalAccessException x) {
throw (IOException) new IOException("Failed to set the custom verb").initCause(x);
}
}
if (!connection.getRequestMethod().equals(method))
throw new IllegalStateException("Failed to set the request method to " + method);
}
/**
* {@inheritDoc}
*/
InputStream bodyStream() throws IOException {
return wrapStream(connection.getInputStream());
}
/**
* {@inheritDoc}
*/
String errorMessage() {
String result = null;
InputStream stream = null;
try {
stream = connection.getErrorStream();
if (stream != null) {
result = IOUtils.toString(wrapStream(stream), StandardCharsets.UTF_8);
}
} catch (Exception e) {
LOGGER.log(FINER, "Ignored exception get error message", e);
} finally {
IOUtils.closeQuietly(stream);
}
return result;
}
/**
* Handles the "Content-Encoding" header.
*
* @param stream
* the stream to possibly wrap
*
*/
private InputStream wrapStream(InputStream stream) throws IOException {
String encoding = headerField("Content-Encoding");
if (encoding == null || stream == null)
return stream;
if (encoding.equals("gzip"))
return new GZIPInputStream(stream);
throw new UnsupportedOperationException("Unexpected Content-Encoding: " + encoding);
}
private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());
@Override
public void close() throws IOException {
IOUtils.closeQuietly(connection.getInputStream());
}
}
}