HttpClientGitHubConnector.java

package org.kohsuke.github.extras;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.connector.GitHubConnector;
import org.kohsuke.github.connector.GitHubConnectorRequest;
import org.kohsuke.github.connector.GitHubConnectorResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
 * {@link GitHubConnector} for {@link HttpClient}.
 *
 * @author Liam Newman
 */
@SuppressFBWarnings(value = { "CT_CONSTRUCTOR_THROW" }, justification = "Basic validation")
public class HttpClientGitHubConnector implements GitHubConnector {

    private final HttpClient client;

    /**
     * Instantiates a new HttpClientGitHubConnector with a default HttpClient.
     */
    public HttpClientGitHubConnector() {
        // GitHubClient handles redirects manually as Java HttpClient copies all the headers when redirecting
        // even when redirecting to a different host which is problematic as we don't want
        // to push the Authorization header when redirected to a different host.
        // This problem was discovered when upload-artifact@v4 was released as the new
        // service we are redirected to for downloading the artifacts doesn't support
        // having the Authorization header set.
        // The new implementation does not push the Authorization header when redirected
        // to a different host, which is similar to what Okhttp is doing:
        // https://github.com/square/okhttp/blob/f9dfd4e8cc070ca2875a67d8f7ad939d95e7e296/okhttp/src/main/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt#L313-L318
        // See also https://github.com/arduino/report-size-deltas/pull/83 for more context
        this(HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build());
    }

    /**
     * Instantiates a new HttpClientGitHubConnector.
     *
     * @param client
     *            the HttpClient to be used
     */
    public HttpClientGitHubConnector(HttpClient client) {
        this.client = client;
    }

    @Override
    public GitHubConnectorResponse send(GitHubConnectorRequest connectorRequest) throws IOException {
        HttpRequest.Builder builder = HttpRequest.newBuilder();
        try {
            builder.uri(connectorRequest.url().toURI());
        } catch (URISyntaxException e) {
            throw new IOException("Invalid URL", e);
        }

        for (Map.Entry<String, List<String>> e : connectorRequest.allHeaders().entrySet()) {
            List<String> v = e.getValue();
            if (v != null) {
                builder.header(e.getKey(), String.join(", ", v));
            }
        }

        HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.noBody();
        if (connectorRequest.hasBody()) {
            publisher = HttpRequest.BodyPublishers.ofByteArray(IOUtils.toByteArray(connectorRequest.body()));
        }
        builder.method(connectorRequest.method(), publisher);

        HttpRequest request = builder.build();

        try {
            HttpResponse<InputStream> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
            return new HttpClientGitHubConnectorResponse(connectorRequest, httpResponse);
        } catch (InterruptedException e) {
            throw (InterruptedIOException) new InterruptedIOException(e.getMessage()).initCause(e);
        }
    }

    /**
     * Initial response information when a response is initially received and before the body is processed.
     *
     * Implementation specific to {@link HttpResponse}.
     */
    private static class HttpClientGitHubConnectorResponse extends GitHubConnectorResponse.ByteArrayResponse {

        @Nonnull
        private final HttpResponse<InputStream> response;

        protected HttpClientGitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
                @Nonnull HttpResponse<InputStream> response) {
            super(request, response.statusCode(), response.headers().map());
            this.response = response;
        }

        @CheckForNull
        @Override
        protected InputStream rawBodyStream() throws IOException {
            return response.body();
        }

        @Override
        public void close() throws IOException {
            super.close();
            IOUtils.closeQuietly(response.body());
        }
    }
}