mirror of
https://github.com/jlengrand/github-api.git
synced 2026-03-11 00:11:25 +00:00
714 lines
40 KiB
HTML
714 lines
40 KiB
HTML
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>GitHubClient.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">GitHub API for Java</a> > <a href="index.source.html" class="el_package">org.kohsuke.github</a> > <span class="el_source">GitHubClient.java</span></div><h1>GitHubClient.java</h1><pre class="source lang-java linenums">package org.kohsuke.github;
|
|
|
|
import com.fasterxml.jackson.databind.*;
|
|
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
|
|
import org.apache.commons.io.IOUtils;
|
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
|
import org.kohsuke.github.authorization.UserAuthorizationProvider;
|
|
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InterruptedIOException;
|
|
import java.net.*;
|
|
import java.time.Instant;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.*;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.function.Consumer;
|
|
import java.util.logging.Logger;
|
|
|
|
import javax.annotation.CheckForNull;
|
|
import javax.annotation.Nonnull;
|
|
import javax.net.ssl.SSLHandshakeException;
|
|
|
|
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
|
|
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
|
|
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
|
import static java.util.logging.Level.*;
|
|
|
|
/**
|
|
* A GitHub API Client
|
|
* <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>
|
|
*/
|
|
abstract class GitHubClient {
|
|
|
|
static final int CONNECTION_ERROR_RETRIES = 2;
|
|
/**
|
|
* If timeout issues let's retry after milliseconds.
|
|
*/
|
|
static final int retryTimeoutMillis = 100;
|
|
/* private */ final String login;
|
|
|
|
// Cache of myself object.
|
|
private final String apiUrl;
|
|
|
|
protected final RateLimitHandler rateLimitHandler;
|
|
protected final AbuseLimitHandler abuseLimitHandler;
|
|
private final GitHubRateLimitChecker rateLimitChecker;
|
|
private final AuthorizationProvider authorizationProvider;
|
|
|
|
private HttpConnector connector;
|
|
|
|
<span class="fc" id="L56"> @Nonnull</span>
|
|
private final AtomicReference<GHRateLimit> rateLimit = new AtomicReference<>(GHRateLimit.DEFAULT);
|
|
|
|
<span class="fc" id="L59"> private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());</span>
|
|
|
|
<span class="fc" id="L61"> private static final ObjectMapper MAPPER = new ObjectMapper();</span>
|
|
static final String GITHUB_URL = "https://api.github.com";
|
|
|
|
<span class="fc" id="L64"> private static final DateTimeFormatter DATE_TIME_PARSER_SLASHES = DateTimeFormatter</span>
|
|
<span class="fc" id="L65"> .ofPattern("yyyy/MM/dd HH:mm:ss Z");</span>
|
|
|
|
static {
|
|
<span class="fc" id="L68"> MAPPER.setVisibility(new VisibilityChecker.Std(NONE, NONE, NONE, NONE, ANY));</span>
|
|
<span class="fc" id="L69"> MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);</span>
|
|
<span class="fc" id="L70"> MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);</span>
|
|
<span class="fc" id="L71"> MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);</span>
|
|
<span class="fc" id="L72"> }</span>
|
|
|
|
GitHubClient(String apiUrl,
|
|
HttpConnector connector,
|
|
RateLimitHandler rateLimitHandler,
|
|
AbuseLimitHandler abuseLimitHandler,
|
|
GitHubRateLimitChecker rateLimitChecker,
|
|
Consumer<GHMyself> myselfConsumer,
|
|
<span class="fc" id="L80"> AuthorizationProvider authorizationProvider) throws IOException {</span>
|
|
|
|
<span class="pc bpc" id="L82" title="1 of 2 branches missed."> if (apiUrl.endsWith("/")) {</span>
|
|
<span class="nc" id="L83"> apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize</span>
|
|
}
|
|
|
|
<span class="fc bfc" id="L86" title="All 2 branches covered."> if (null == connector) {</span>
|
|
<span class="fc" id="L87"> connector = HttpConnector.DEFAULT;</span>
|
|
}
|
|
<span class="fc" id="L89"> this.apiUrl = apiUrl;</span>
|
|
<span class="fc" id="L90"> this.connector = connector;</span>
|
|
|
|
// Prefer credential configuration via provider
|
|
<span class="fc" id="L93"> this.authorizationProvider = authorizationProvider;</span>
|
|
|
|
<span class="fc" id="L95"> this.rateLimitHandler = rateLimitHandler;</span>
|
|
<span class="fc" id="L96"> this.abuseLimitHandler = abuseLimitHandler;</span>
|
|
<span class="fc" id="L97"> this.rateLimitChecker = rateLimitChecker;</span>
|
|
|
|
<span class="fc" id="L99"> this.login = getCurrentUser(myselfConsumer);</span>
|
|
<span class="fc" id="L100"> }</span>
|
|
|
|
private String getCurrentUser(Consumer<GHMyself> myselfConsumer) throws IOException {
|
|
<span class="fc" id="L103"> String login = null;</span>
|
|
<span class="fc bfc" id="L104" title="All 2 branches covered."> if (this.authorizationProvider instanceof UserAuthorizationProvider</span>
|
|
<span class="pc bpc" id="L105" title="1 of 2 branches missed."> && this.authorizationProvider.getEncodedAuthorization() != null) {</span>
|
|
|
|
<span class="fc" id="L107"> UserAuthorizationProvider userAuthorizationProvider = (UserAuthorizationProvider) this.authorizationProvider;</span>
|
|
|
|
<span class="fc" id="L109"> login = userAuthorizationProvider.getLogin();</span>
|
|
|
|
<span class="pc bpc" id="L111" title="1 of 2 branches missed."> if (login == null) {</span>
|
|
try {
|
|
<span class="nc" id="L113"> GHMyself myself = fetch(GHMyself.class, "/user");</span>
|
|
<span class="nc bnc" id="L114" title="All 2 branches missed."> if (myselfConsumer != null) {</span>
|
|
<span class="nc" id="L115"> myselfConsumer.accept(myself);</span>
|
|
}
|
|
<span class="nc" id="L117"> login = myself.getLogin();</span>
|
|
<span class="nc" id="L118"> } catch (IOException e) {</span>
|
|
<span class="nc" id="L119"> return null;</span>
|
|
<span class="nc" id="L120"> }</span>
|
|
}
|
|
}
|
|
<span class="fc" id="L123"> return login;</span>
|
|
}
|
|
|
|
private <T> T fetch(Class<T> type, String urlPath) throws IOException {
|
|
<span class="fc" id="L127"> GitHubRequest request = GitHubRequest.newBuilder().withApiUrl(getApiUrl()).withUrlPath(urlPath).build();</span>
|
|
<span class="fc" id="L128"> return this.sendRequest(request, (responseInfo) -> GitHubResponse.parseBody(responseInfo, type)).body();</span>
|
|
}
|
|
|
|
/**
|
|
* Ensures that the credential for this client is valid.
|
|
*
|
|
* @return the boolean
|
|
*/
|
|
public boolean isCredentialValid() {
|
|
try {
|
|
// If 404, ratelimit returns a default value.
|
|
// This works as credential test because invalid credentials returns 401, not 404
|
|
<span class="fc" id="L140"> getRateLimit();</span>
|
|
<span class="fc" id="L141"> return true;</span>
|
|
<span class="fc" id="L142"> } catch (IOException e) {</span>
|
|
<span class="fc" id="L143"> LOGGER.log(FINE,</span>
|
|
<span class="fc" id="L144"> "Exception validating credentials on " + getApiUrl() + " with login '" + login + "' " + e,</span>
|
|
e);
|
|
<span class="fc" id="L146"> return false;</span>
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is this an always offline "connection".
|
|
*
|
|
* @return {@code true} if this is an always offline "connection".
|
|
*/
|
|
public boolean isOffline() {
|
|
<span class="fc bfc" id="L156" title="All 2 branches covered."> return getConnector() == HttpConnector.OFFLINE;</span>
|
|
}
|
|
|
|
/**
|
|
* Gets connector.
|
|
*
|
|
* @return the connector
|
|
*/
|
|
public HttpConnector getConnector() {
|
|
<span class="fc" id="L165"> return connector;</span>
|
|
}
|
|
|
|
/**
|
|
* Sets the custom connector used to make requests to GitHub.
|
|
*
|
|
* @param connector
|
|
* the connector
|
|
* @deprecated HttpConnector should not be changed.
|
|
*/
|
|
@Deprecated
|
|
public void setConnector(HttpConnector connector) {
|
|
<span class="nc" id="L177"> LOGGER.warning("Connector should not be changed. Please file an issue describing your use case.");</span>
|
|
<span class="nc" id="L178"> this.connector = connector;</span>
|
|
<span class="nc" id="L179"> }</span>
|
|
|
|
/**
|
|
* Is this an anonymous connection
|
|
*
|
|
* @return {@code true} if operations that require authentication will fail.
|
|
*/
|
|
public boolean isAnonymous() {
|
|
try {
|
|
<span class="pc bpc" id="L188" title="1 of 4 branches missed."> return login == null && this.authorizationProvider.getEncodedAuthorization() == null;</span>
|
|
<span class="nc" id="L189"> } catch (IOException e) {</span>
|
|
// An exception here means that the provider failed to provide authorization parameters,
|
|
// basically meaning the same as "no auth"
|
|
<span class="nc" id="L192"> return false;</span>
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the current full rate limit information from the server.
|
|
*
|
|
* For some versions of GitHub Enterprise, the {@code /rate_limit} endpoint returns a {@code 404 Not Found}. In that
|
|
* case, the most recent {@link GHRateLimit} information will be returned, including rate limit information returned
|
|
* in the response header for this request in if was present.
|
|
*
|
|
* For most use cases it would be better to implement a {@link RateLimitChecker} and add it via
|
|
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
|
|
*
|
|
* @return the rate limit
|
|
* @throws IOException
|
|
* the io exception
|
|
*/
|
|
@Nonnull
|
|
public GHRateLimit getRateLimit() throws IOException {
|
|
<span class="fc" id="L212"> return getRateLimit(RateLimitTarget.NONE);</span>
|
|
}
|
|
|
|
@CheckForNull
|
|
protected String getEncodedAuthorization() throws IOException {
|
|
<span class="fc" id="L217"> return authorizationProvider.getEncodedAuthorization();</span>
|
|
}
|
|
|
|
@Nonnull
|
|
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
|
|
GHRateLimit result;
|
|
try {
|
|
<span class="fc" id="L224"> GitHubRequest request = GitHubRequest.newBuilder()</span>
|
|
<span class="fc" id="L225"> .rateLimit(RateLimitTarget.NONE)</span>
|
|
<span class="fc" id="L226"> .withApiUrl(getApiUrl())</span>
|
|
<span class="fc" id="L227"> .withUrlPath("/rate_limit")</span>
|
|
<span class="fc" id="L228"> .build();</span>
|
|
<span class="fc" id="L229"> result = this</span>
|
|
<span class="fc" id="L230"> .sendRequest(request, (responseInfo) -> GitHubResponse.parseBody(responseInfo, JsonRateLimit.class))</span>
|
|
<span class="fc" id="L231"> .body().resources;</span>
|
|
<span class="fc" id="L232"> } catch (FileNotFoundException e) {</span>
|
|
// For some versions of GitHub Enterprise, the rate_limit endpoint returns a 404.
|
|
<span class="fc" id="L234"> LOGGER.log(FINE, "/rate_limit returned 404 Not Found.");</span>
|
|
|
|
// However some newer versions of GHE include rate limit header information
|
|
// If the header info is missing and the endpoint returns 404, fill the rate limit
|
|
// with unknown
|
|
<span class="fc" id="L239"> result = GHRateLimit.fromRecord(GHRateLimit.UnknownLimitRecord.current(), rateLimitTarget);</span>
|
|
<span class="fc" id="L240"> }</span>
|
|
<span class="fc" id="L241"> return updateRateLimit(result);</span>
|
|
}
|
|
|
|
/**
|
|
* Returns the most recently observed rate limit data.
|
|
*
|
|
* Generally, instead of calling this you should implement a {@link RateLimitChecker} or call
|
|
*
|
|
* @return the most recently observed rate limit data. This may include expired or
|
|
* {@link GHRateLimit.UnknownLimitRecord} entries.
|
|
* @deprecated implement a {@link RateLimitChecker} and add it via
|
|
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
|
|
*/
|
|
@Nonnull
|
|
@Deprecated
|
|
GHRateLimit lastRateLimit() {
|
|
<span class="fc" id="L257"> return rateLimit.get();</span>
|
|
}
|
|
|
|
/**
|
|
* Gets the current rate limit for an endpoint while trying not to actually make any remote requests unless
|
|
* absolutely necessary.
|
|
*
|
|
* If the {@link GHRateLimit.Record} for {@code urlPath} is not expired, it is returned. If the
|
|
* {@link GHRateLimit.Record} for {@code urlPath} is expired, {@link #getRateLimit()} will be called to get the
|
|
* current rate limit.
|
|
*
|
|
* @param rateLimitTarget
|
|
* the endpoint to get the rate limit for.
|
|
*
|
|
* @return the current rate limit data. {@link GHRateLimit.Record}s in this instance may be expired when returned.
|
|
* @throws IOException
|
|
* if there was an error getting current rate limit data.
|
|
*/
|
|
@Nonnull
|
|
GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
|
|
<span class="fc" id="L277"> GHRateLimit result = rateLimit.get();</span>
|
|
// Most of the time rate limit is not expired, so try to avoid locking.
|
|
<span class="fc bfc" id="L279" title="All 2 branches covered."> if (result.getRecord(rateLimitTarget).isExpired()) {</span>
|
|
// if the rate limit is expired, synchronize to ensure
|
|
// only one call to getRateLimit() is made to refresh it.
|
|
<span class="fc" id="L282"> synchronized (this) {</span>
|
|
<span class="pc bpc" id="L283" title="1 of 2 branches missed."> if (rateLimit.get().getRecord(rateLimitTarget).isExpired()) {</span>
|
|
<span class="fc" id="L284"> getRateLimit(rateLimitTarget);</span>
|
|
}
|
|
<span class="fc" id="L286"> }</span>
|
|
<span class="fc" id="L287"> result = rateLimit.get();</span>
|
|
}
|
|
<span class="fc" id="L289"> return result;</span>
|
|
}
|
|
|
|
/**
|
|
* Update the Rate Limit with the latest info from response header.
|
|
*
|
|
* Due to multi-threading, requests might complete out of order. This method calls
|
|
* {@link GHRateLimit#getMergedRateLimit(GHRateLimit)} to ensure the most current records are used.
|
|
*
|
|
* @param observed
|
|
* {@link GHRateLimit.Record} constructed from the response header information
|
|
*/
|
|
private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {
|
|
<span class="fc" id="L302"> GHRateLimit result = rateLimit.accumulateAndGet(observed, (current, x) -> current.getMergedRateLimit(x));</span>
|
|
<span class="fc" id="L303"> LOGGER.log(FINEST, "Rate limit now: {0}", rateLimit.get());</span>
|
|
<span class="fc" id="L304"> return result;</span>
|
|
}
|
|
|
|
/**
|
|
* Tests the connection.
|
|
*
|
|
* <p>
|
|
* Verify that the API URL and credentials are valid to access this GitHub.
|
|
*
|
|
* <p>
|
|
* This method returns normally if the endpoint is reachable and verified to be GitHub API URL. Otherwise this
|
|
* method throws {@link IOException} to indicate the problem.
|
|
*
|
|
* @throws IOException
|
|
* the io exception
|
|
*/
|
|
public void checkApiUrlValidity() throws IOException {
|
|
try {
|
|
<span class="fc" id="L322"> fetch(GHApiInfo.class, "/").check(getApiUrl());</span>
|
|
<span class="fc" id="L323"> } catch (IOException e) {</span>
|
|
<span class="fc bfc" id="L324" title="All 2 branches covered."> if (isPrivateModeEnabled()) {</span>
|
|
<span class="fc" id="L325"> throw (IOException) new IOException(</span>
|
|
<span class="fc" id="L326"> "GitHub Enterprise server (" + getApiUrl() + ") with private mode enabled").initCause(e);</span>
|
|
}
|
|
<span class="fc" id="L328"> throw e;</span>
|
|
<span class="fc" id="L329"> }</span>
|
|
<span class="fc" id="L330"> }</span>
|
|
|
|
public String getApiUrl() {
|
|
<span class="fc" id="L333"> return apiUrl;</span>
|
|
}
|
|
|
|
/**
|
|
* Builds a {@link GitHubRequest}, sends the {@link GitHubRequest} to the server, and uses the
|
|
* {@link GitHubResponse.BodyHandler} to parse the response info and response body data into an instance of
|
|
* {@link T}.
|
|
*
|
|
* @param builder
|
|
* used to build the request that will be sent to the server.
|
|
* @param handler
|
|
* parse the response info and body data into a instance of {@link T}. If null, no parsing occurs and
|
|
* {@link GitHubResponse#body()} will return null.
|
|
* @param <T>
|
|
* the type of the parse body data.
|
|
* @return a {@link GitHubResponse} containing the parsed body data as a {@link T}. Parsed instance may be null.
|
|
* @throws IOException
|
|
* if an I/O Exception occurs
|
|
*/
|
|
@Nonnull
|
|
public <T> GitHubResponse<T> sendRequest(@Nonnull GitHubRequest.Builder<?> builder,
|
|
@CheckForNull GitHubResponse.BodyHandler<T> handler) throws IOException {
|
|
<span class="fc" id="L355"> return sendRequest(builder.build(), handler);</span>
|
|
}
|
|
|
|
/**
|
|
* Sends the {@link GitHubRequest} to the server, and uses the {@link GitHubResponse.BodyHandler} to parse the
|
|
* response info and response body data into an instance of {@link T}.
|
|
*
|
|
* @param request
|
|
* the request that will be sent to the server.
|
|
* @param handler
|
|
* parse the response info and body data into a instance of {@link T}. If null, no parsing occurs and
|
|
* {@link GitHubResponse#body()} will return null.
|
|
* @param <T>
|
|
* the type of the parse body data.
|
|
* @return a {@link GitHubResponse} containing the parsed body data as a {@link T}. Parsed instance may be null.
|
|
* @throws IOException
|
|
* if an I/O Exception occurs
|
|
*/
|
|
@Nonnull
|
|
public <T> GitHubResponse<T> sendRequest(GitHubRequest request, @CheckForNull GitHubResponse.BodyHandler<T> handler)
|
|
throws IOException {
|
|
<span class="fc" id="L376"> int retries = CONNECTION_ERROR_RETRIES;</span>
|
|
|
|
do {
|
|
// if we fail to create a connection we do not retry and we do not wrap
|
|
|
|
<span class="fc" id="L381"> GitHubResponse.ResponseInfo responseInfo = null;</span>
|
|
try {
|
|
try {
|
|
<span class="fc" id="L384"> logRequest(request);</span>
|
|
<span class="fc" id="L385"> rateLimitChecker.checkRateLimit(this, request);</span>
|
|
|
|
<span class="fc" id="L387"> responseInfo = getResponseInfo(request);</span>
|
|
<span class="fc" id="L388"> noteRateLimit(responseInfo);</span>
|
|
<span class="fc" id="L389"> detectOTPRequired(responseInfo);</span>
|
|
|
|
<span class="fc bfc" id="L391" title="All 2 branches covered."> if (isInvalidCached404Response(responseInfo)) {</span>
|
|
// Setting "Cache-Control" to "no-cache" stops the cache from supplying
|
|
// "If-Modified-Since" or "If-None-Match" values.
|
|
// This makes GitHub give us current data (not incorrectly cached data)
|
|
<span class="fc" id="L395"> request = request.toBuilder().setHeader("Cache-Control", "no-cache").build();</span>
|
|
continue;
|
|
}
|
|
<span class="fc bfc" id="L398" title="All 4 branches covered."> if (!(isRateLimitResponse(responseInfo) || isAbuseLimitResponse(responseInfo))) {</span>
|
|
<span class="fc" id="L399"> return createResponse(responseInfo, handler);</span>
|
|
}
|
|
<span class="fc" id="L401"> } catch (IOException e) {</span>
|
|
// For transient errors, retry
|
|
<span class="fc bfc" id="L403" title="All 2 branches covered."> if (retryConnectionError(e, request.url(), retries)) {</span>
|
|
continue;
|
|
}
|
|
|
|
<span class="fc" id="L407"> throw interpretApiError(e, request, responseInfo);</span>
|
|
<span class="fc" id="L408"> }</span>
|
|
|
|
<span class="fc" id="L410"> handleLimitingErrors(responseInfo);</span>
|
|
} finally {
|
|
<span class="fc" id="L412"> IOUtils.closeQuietly(responseInfo);</span>
|
|
}
|
|
|
|
<span class="fc bfc" id="L415" title="All 2 branches covered."> } while (--retries >= 0);</span>
|
|
|
|
<span class="fc" id="L417"> throw new GHIOException("Ran out of retries for URL: " + request.url().toString());</span>
|
|
}
|
|
|
|
private void logRequest(@Nonnull final GitHubRequest request) {
|
|
<span class="fc" id="L421"> LOGGER.log(FINE,</span>
|
|
<span class="nc bnc" id="L422" title="All 2 branches missed."> () -> "GitHub API request [" + (login == null ? "anonymous" : login) + "]: " + request.method() + " "</span>
|
|
<span class="nc" id="L423"> + request.url().toString());</span>
|
|
<span class="fc" id="L424"> }</span>
|
|
|
|
@Nonnull
|
|
protected abstract GitHubResponse.ResponseInfo getResponseInfo(GitHubRequest request) throws IOException;
|
|
|
|
protected abstract void handleLimitingErrors(@Nonnull GitHubResponse.ResponseInfo responseInfo) throws IOException;
|
|
|
|
@Nonnull
|
|
private static <T> GitHubResponse<T> createResponse(@Nonnull GitHubResponse.ResponseInfo responseInfo,
|
|
@CheckForNull GitHubResponse.BodyHandler<T> handler) throws IOException {
|
|
<span class="fc" id="L434"> T body = null;</span>
|
|
<span class="pc bpc" id="L435" title="1 of 2 branches missed."> if (responseInfo.statusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {</span>
|
|
// special case handling for 304 unmodified, as the content will be ""
|
|
<span class="fc bfc" id="L437" title="All 2 branches covered."> } else if (responseInfo.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {</span>
|
|
|
|
// Response code 202 means data is being generated or an action that can require some time is triggered.
|
|
// This happens in specific cases:
|
|
// statistics - See https://developer.github.com/v3/repos/statistics/#a-word-about-caching
|
|
// fork creation - See https://developer.github.com/v3/repos/forks/#create-a-fork
|
|
// workflow run cancellation - See https://docs.github.com/en/rest/reference/actions#cancel-a-workflow-run
|
|
|
|
<span class="fc" id="L445"> LOGGER.log(FINE,</span>
|
|
<span class="fc" id="L446"> "Received HTTP_ACCEPTED(202) from " + responseInfo.url().toString()</span>
|
|
+ " . Please try again in 5 seconds.");
|
|
<span class="fc bfc" id="L448" title="All 2 branches covered."> } else if (handler != null) {</span>
|
|
<span class="fc" id="L449"> body = handler.apply(responseInfo);</span>
|
|
}
|
|
<span class="fc" id="L451"> return new GitHubResponse<>(responseInfo, body);</span>
|
|
}
|
|
|
|
/**
|
|
* Handle API error by either throwing it or by returning normally to retry.
|
|
*/
|
|
private static IOException interpretApiError(IOException e,
|
|
@Nonnull GitHubRequest request,
|
|
@CheckForNull GitHubResponse.ResponseInfo responseInfo) throws IOException {
|
|
// If we're already throwing a GHIOException, pass through
|
|
<span class="fc bfc" id="L461" title="All 2 branches covered."> if (e instanceof GHIOException) {</span>
|
|
<span class="fc" id="L462"> return e;</span>
|
|
}
|
|
|
|
<span class="fc" id="L465"> int statusCode = -1;</span>
|
|
<span class="fc" id="L466"> String message = null;</span>
|
|
<span class="fc" id="L467"> Map<String, List<String>> headers = new HashMap<>();</span>
|
|
<span class="fc" id="L468"> String errorMessage = null;</span>
|
|
|
|
<span class="fc bfc" id="L470" title="All 2 branches covered."> if (responseInfo != null) {</span>
|
|
<span class="fc" id="L471"> statusCode = responseInfo.statusCode();</span>
|
|
<span class="fc" id="L472"> message = responseInfo.headerField("Status");</span>
|
|
<span class="fc" id="L473"> headers = responseInfo.headers();</span>
|
|
<span class="fc" id="L474"> errorMessage = responseInfo.errorMessage();</span>
|
|
}
|
|
|
|
<span class="fc bfc" id="L477" title="All 2 branches covered."> if (errorMessage != null) {</span>
|
|
<span class="fc bfc" id="L478" title="All 2 branches covered."> if (e instanceof FileNotFoundException) {</span>
|
|
// pass through 404 Not Found to allow the caller to handle it intelligently
|
|
<span class="fc" id="L480"> e = new GHFileNotFoundException(e.getMessage() + " " + errorMessage, e)</span>
|
|
<span class="fc" id="L481"> .withResponseHeaderFields(headers);</span>
|
|
<span class="pc bpc" id="L482" title="1 of 2 branches missed."> } else if (statusCode >= 0) {</span>
|
|
<span class="fc" id="L483"> e = new HttpException(errorMessage, statusCode, message, request.url().toString(), e);</span>
|
|
} else {
|
|
<span class="nc" id="L485"> e = new GHIOException(errorMessage).withResponseHeaderFields(headers);</span>
|
|
}
|
|
<span class="fc bfc" id="L487" title="All 2 branches covered."> } else if (!(e instanceof FileNotFoundException)) {</span>
|
|
<span class="fc" id="L488"> e = new HttpException(statusCode, message, request.url().toString(), e);</span>
|
|
}
|
|
<span class="fc" id="L490"> return e;</span>
|
|
}
|
|
|
|
protected static boolean isRateLimitResponse(@Nonnull GitHubResponse.ResponseInfo responseInfo) {
|
|
<span class="fc bfc" id="L494" title="All 2 branches covered."> return responseInfo.statusCode() == HttpURLConnection.HTTP_FORBIDDEN</span>
|
|
<span class="fc bfc" id="L495" title="All 2 branches covered."> && "0".equals(responseInfo.headerField("X-RateLimit-Remaining"));</span>
|
|
}
|
|
|
|
protected static boolean isAbuseLimitResponse(@Nonnull GitHubResponse.ResponseInfo responseInfo) {
|
|
<span class="fc bfc" id="L499" title="All 2 branches covered."> return responseInfo.statusCode() == HttpURLConnection.HTTP_FORBIDDEN</span>
|
|
<span class="fc bfc" id="L500" title="All 2 branches covered."> && responseInfo.headerField("Retry-After") != null;</span>
|
|
}
|
|
|
|
private static boolean retryConnectionError(IOException e, URL url, int retries) throws IOException {
|
|
// There are a range of connection errors where we want to wait a moment and just automatically retry
|
|
<span class="fc bfc" id="L505" title="All 6 branches covered."> boolean connectionError = e instanceof SocketException || e instanceof SocketTimeoutException</span>
|
|
|| e instanceof SSLHandshakeException;
|
|
<span class="fc bfc" id="L507" title="All 4 branches covered."> if (connectionError && retries > 0) {</span>
|
|
<span class="fc" id="L508"> LOGGER.log(INFO,</span>
|
|
<span class="fc" id="L509"> e.getMessage() + " while connecting to " + url + ". Sleeping " + GitHubClient.retryTimeoutMillis</span>
|
|
+ " milliseconds before retrying... ; will try " + retries + " more time(s)");
|
|
try {
|
|
<span class="fc" id="L512"> Thread.sleep(GitHubClient.retryTimeoutMillis);</span>
|
|
<span class="nc" id="L513"> } catch (InterruptedException ie) {</span>
|
|
<span class="nc" id="L514"> throw (IOException) new InterruptedIOException().initCause(e);</span>
|
|
<span class="fc" id="L515"> }</span>
|
|
<span class="fc" id="L516"> return true;</span>
|
|
}
|
|
<span class="fc" id="L518"> return false;</span>
|
|
}
|
|
|
|
private static boolean isInvalidCached404Response(GitHubResponse.ResponseInfo responseInfo) {
|
|
// WORKAROUND FOR ISSUE #669:
|
|
// When the Requester detects a 404 response with an ETag (only happpens when the server's 304
|
|
// is bogus and would cause cache corruption), try the query again with new request header
|
|
// that forces the server to not return 304 and return new data instead.
|
|
//
|
|
// This solution is transparent to users of this library and automatically handles a
|
|
// situation that was cause insidious and hard to debug bad responses in caching
|
|
// scenarios. If GitHub ever fixes their issue and/or begins providing accurate ETags to
|
|
// their 404 responses, this will result in at worst two requests being made for each 404
|
|
// responses. However, only the second request will count against rate limit.
|
|
<span class="fc bfc" id="L532" title="All 4 branches covered."> if (responseInfo.statusCode() == 404 && Objects.equals(responseInfo.request().method(), "GET")</span>
|
|
<span class="fc bfc" id="L533" title="All 2 branches covered."> && responseInfo.headerField("ETag") != null</span>
|
|
<span class="pc bpc" id="L534" title="1 of 2 branches missed."> && !Objects.equals(responseInfo.request().headers().get("Cache-Control"), "no-cache")) {</span>
|
|
<span class="fc" id="L535"> LOGGER.log(FINE,</span>
|
|
<span class="fc" id="L536"> "Encountered GitHub invalid cached 404 from " + responseInfo.url()</span>
|
|
+ ". Retrying with \"Cache-Control\"=\"no-cache\"...");
|
|
<span class="fc" id="L538"> return true;</span>
|
|
}
|
|
<span class="fc" id="L540"> return false;</span>
|
|
}
|
|
|
|
private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo responseInfo) {
|
|
try {
|
|
<span class="fc" id="L545"> String limitString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Limit"),</span>
|
|
"Missing X-RateLimit-Limit");
|
|
<span class="fc" id="L547"> String remainingString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Remaining"),</span>
|
|
"Missing X-RateLimit-Remaining");
|
|
<span class="fc" id="L549"> String resetString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Reset"),</span>
|
|
"Missing X-RateLimit-Reset");
|
|
int limit, remaining;
|
|
long reset;
|
|
<span class="fc" id="L553"> limit = Integer.parseInt(limitString);</span>
|
|
<span class="fc" id="L554"> remaining = Integer.parseInt(remainingString);</span>
|
|
<span class="fc" id="L555"> reset = Long.parseLong(resetString);</span>
|
|
<span class="fc" id="L556"> GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);</span>
|
|
<span class="fc" id="L557"> updateRateLimit(GHRateLimit.fromRecord(observed, responseInfo.request().rateLimitTarget()));</span>
|
|
<span class="fc" id="L558"> } catch (NumberFormatException | NullPointerException e) {</span>
|
|
<span class="fc" id="L559"> LOGGER.log(FINEST, "Missing or malformed X-RateLimit header: ", e);</span>
|
|
<span class="fc" id="L560"> }</span>
|
|
<span class="fc" id="L561"> }</span>
|
|
|
|
private static void detectOTPRequired(@Nonnull GitHubResponse.ResponseInfo responseInfo) throws GHIOException {
|
|
// 401 Unauthorized == bad creds or OTP request
|
|
<span class="fc bfc" id="L565" title="All 2 branches covered."> if (responseInfo.statusCode() == HTTP_UNAUTHORIZED) {</span>
|
|
// In the case of a user with 2fa enabled, a header with X-GitHub-OTP
|
|
// will be returned indicating the user needs to respond with an otp
|
|
<span class="fc bfc" id="L568" title="All 2 branches covered."> if (responseInfo.headerField("X-GitHub-OTP") != null) {</span>
|
|
<span class="fc" id="L569"> throw new GHOTPRequiredException().withResponseHeaderFields(responseInfo.headers());</span>
|
|
}
|
|
}
|
|
<span class="fc" id="L572"> }</span>
|
|
|
|
void requireCredential() {
|
|
<span class="fc bfc" id="L575" title="All 2 branches covered."> if (isAnonymous())</span>
|
|
<span class="fc" id="L576"> throw new IllegalStateException(</span>
|
|
"This operation requires a credential but none is given to the GitHub constructor");
|
|
<span class="fc" id="L578"> }</span>
|
|
|
|
private static class GHApiInfo {
|
|
private String rate_limit_url;
|
|
|
|
void check(String apiUrl) throws IOException {
|
|
<span class="fc bfc" id="L584" title="All 2 branches covered."> if (rate_limit_url == null)</span>
|
|
<span class="fc" id="L585"> throw new IOException(apiUrl + " doesn't look like GitHub API URL");</span>
|
|
|
|
// make sure that the URL is legitimate
|
|
<span class="fc" id="L588"> new URL(rate_limit_url);</span>
|
|
<span class="fc" id="L589"> }</span>
|
|
}
|
|
|
|
/**
|
|
* Checks if a GitHub Enterprise server is configured in private mode.
|
|
*
|
|
* In private mode response looks like:
|
|
*
|
|
* <pre>
|
|
* $ curl -i https://github.mycompany.com/api/v3/
|
|
* HTTP/1.1 401 Unauthorized
|
|
* Server: GitHub.com
|
|
* Date: Sat, 05 Mar 2016 19:45:01 GMT
|
|
* Content-Type: application/json; charset=utf-8
|
|
* Content-Length: 130
|
|
* Status: 401 Unauthorized
|
|
* X-GitHub-Media-Type: github.v3
|
|
* X-XSS-Protection: 1; mode=block
|
|
* X-Frame-Options: deny
|
|
* Content-Security-Policy: default-src 'none'
|
|
* Access-Control-Allow-Credentials: true
|
|
* Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
|
* Access-Control-Allow-Origin: *
|
|
* X-GitHub-Request-Id: dbc70361-b11d-4131-9a7f-674b8edd0411
|
|
* Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
|
|
* X-Content-Type-Options: nosniff
|
|
* </pre>
|
|
*
|
|
* @return {@code true} if private mode is enabled. If it tries to use this method with GitHub, returns {@code
|
|
* false}.
|
|
*/
|
|
private boolean isPrivateModeEnabled() {
|
|
try {
|
|
<span class="fc" id="L622"> GitHubResponse<?> response = sendRequest(GitHubRequest.newBuilder().withApiUrl(getApiUrl()), null);</span>
|
|
<span class="pc bpc" id="L623" title="1 of 4 branches missed."> return response.statusCode() == HTTP_UNAUTHORIZED && response.headerField("X-GitHub-Media-Type") != null;</span>
|
|
<span class="nc" id="L624"> } catch (IOException e) {</span>
|
|
<span class="nc" id="L625"> return false;</span>
|
|
}
|
|
}
|
|
|
|
static URL parseURL(String s) {
|
|
try {
|
|
<span class="fc bfc" id="L631" title="All 2 branches covered."> return s == null ? null : new URL(s);</span>
|
|
<span class="fc" id="L632"> } catch (MalformedURLException e) {</span>
|
|
<span class="fc" id="L633"> throw new IllegalStateException("Invalid URL: " + s);</span>
|
|
}
|
|
}
|
|
|
|
static Date parseDate(String timestamp) {
|
|
<span class="fc bfc" id="L638" title="All 2 branches covered."> if (timestamp == null)</span>
|
|
<span class="fc" id="L639"> return null;</span>
|
|
|
|
<span class="fc" id="L641"> return Date.from(parseInstant(timestamp));</span>
|
|
}
|
|
|
|
static Instant parseInstant(String timestamp) {
|
|
<span class="fc bfc" id="L645" title="All 2 branches covered."> if (timestamp == null)</span>
|
|
<span class="fc" id="L646"> return null;</span>
|
|
|
|
<span class="fc bfc" id="L648" title="All 2 branches covered."> if (timestamp.charAt(4) == '/') {</span>
|
|
// Unsure where this is used, but retained for compatibility.
|
|
<span class="fc" id="L650"> return Instant.from(DATE_TIME_PARSER_SLASHES.parse(timestamp));</span>
|
|
} else {
|
|
<span class="fc" id="L652"> return Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(timestamp));</span>
|
|
}
|
|
}
|
|
|
|
static String printDate(Date dt) {
|
|
<span class="fc" id="L657"> return DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(dt.getTime()).truncatedTo(ChronoUnit.SECONDS));</span>
|
|
}
|
|
|
|
/**
|
|
* Gets an {@link ObjectWriter}.
|
|
*
|
|
* @return an {@link ObjectWriter} instance that can be further configured.
|
|
*/
|
|
@Nonnull
|
|
static ObjectWriter getMappingObjectWriter() {
|
|
<span class="fc" id="L667"> return MAPPER.writer();</span>
|
|
}
|
|
|
|
/**
|
|
* Helper for {@link #getMappingObjectReader(GitHubResponse.ResponseInfo)}
|
|
*
|
|
* @param root
|
|
* the root GitHub object for this reader
|
|
*
|
|
* @return an {@link ObjectReader} instance that can be further configured.
|
|
*/
|
|
@Nonnull
|
|
static ObjectReader getMappingObjectReader(@Nonnull GitHub root) {
|
|
<span class="fc" id="L680"> ObjectReader reader = getMappingObjectReader((GitHubResponse.ResponseInfo) null);</span>
|
|
<span class="fc" id="L681"> ((InjectableValues.Std) reader.getInjectableValues()).addValue(GitHub.class, root);</span>
|
|
<span class="fc" id="L682"> return reader;</span>
|
|
}
|
|
|
|
/**
|
|
* Gets an {@link ObjectReader}.
|
|
*
|
|
* Members of {@link InjectableValues} must be present even if {@code null}, otherwise classes expecting those
|
|
* values will fail to read. This differs from regular JSONProperties which provide defaults instead of failing.
|
|
*
|
|
* Having one spot to create readers and having it take all injectable values is not a great long term solution but
|
|
* it is sufficient for this first cut.
|
|
*
|
|
* @param responseInfo
|
|
* the {@link GitHubResponse.ResponseInfo} to inject for this reader.
|
|
*
|
|
* @return an {@link ObjectReader} instance that can be further configured.
|
|
*/
|
|
@Nonnull
|
|
static ObjectReader getMappingObjectReader(@CheckForNull GitHubResponse.ResponseInfo responseInfo) {
|
|
<span class="fc" id="L701"> Map<String, Object> injected = new HashMap<>();</span>
|
|
|
|
// Required or many things break
|
|
<span class="fc" id="L704"> injected.put(GitHubResponse.ResponseInfo.class.getName(), null);</span>
|
|
<span class="fc" id="L705"> injected.put(GitHub.class.getName(), null);</span>
|
|
|
|
<span class="fc bfc" id="L707" title="All 2 branches covered."> if (responseInfo != null) {</span>
|
|
<span class="fc" id="L708"> injected.put(GitHubResponse.ResponseInfo.class.getName(), responseInfo);</span>
|
|
<span class="fc" id="L709"> injected.putAll(responseInfo.request().injectedMappingValues());</span>
|
|
}
|
|
<span class="fc" id="L711"> return MAPPER.reader(new InjectableValues.Std(injected));</span>
|
|
}
|
|
}
|
|
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.7.202105040129</span></div></body></html> |