Repository: hub4j/github-api
Pull request #1298 (2021-11-16 21:43:50)

Deprecate limit handlers using HttpURLConnection
# Description

This change completes the deprecation of all classes and methods that use `HttpURLConnection`. 


# Before submitting a PR:
We love getting PRs, but we hate asking people for the same basic changes every time.

- [ ] Push your changes to a branch other than `main`. Create your PR from that branch.
- [ ] Add JavaDocs and other comments
- [ ] Write tests that run and pass in CI. See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to capture snapshot data.
- [ ] Run `mvn -D enable-ci clean install site` locally. If this command doesn't succeed, your change will not pass CI.

# When creating a PR: 

- [ ] Fill in the "Description" above.
- [ ] Enable "Allow edits from maintainers".



Patch:
diff --git a/src/main/java/org/kohsuke/github/AbuseLimitHandler.java b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java
index dfbee7f1d..86be06db0 100644
--- a/src/main/java/org/kohsuke/github/AbuseLimitHandler.java
+++ b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java
@@ -12,11 +12,13 @@ import javax.annotation.Nonnull;
  * Pluggable strategy to determine what to do when the API abuse limit is hit.
  *
  * @author Kohsuke Kawaguchi
- * @see GitHubBuilder#withAbuseLimitHandler(AbuseLimitHandler) GitHubBuilder#withAbuseLimitHandler(AbuseLimitHandler)
+ * @see GitHubBuilder#withAbuseLimitHandler(GitHubAbuseLimitHandler)
+ *      GitHubBuilder#withAbuseLimitHandler(GitHubAbuseLimitHandler)
  * @see <a href="https://developer.github.com/v3/#abuse-rate-limits">documentation</a>
  * @see RateLimitHandler
  */
-public abstract class AbuseLimitHandler {
+@Deprecated
+public abstract class AbuseLimitHandler extends GitHubAbuseLimitHandler {
 
     /**
      * Called when the library encounters HTTP error indicating that the API abuse limit is reached.
@@ -37,7 +39,7 @@ public abstract class AbuseLimitHandler {
      *
      */
     public void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException {
-        GHIOException e = new HttpException("Abuse limit violation",
+        GHIOException e = new HttpException("Abuse limit reached",
                 connectorResponse.statusCode(),
                 connectorResponse.header("Status"),
                 connectorResponse.request().url().toString()).withResponseHeaderFields(connectorResponse.allHeaders());
@@ -66,12 +68,12 @@ public abstract class AbuseLimitHandler {
      *
      */
     @Deprecated
-    public void onError(IOException e, HttpURLConnection uc) throws IOException {
-    }
+    public abstract void onError(IOException e, HttpURLConnection uc) throws IOException;
 
     /**
      * Wait until the API abuse "wait time" is passed.
      */
+    @Deprecated
     public static final AbuseLimitHandler WAIT = new AbuseLimitHandler() {
         @Override
         public void onError(IOException e, HttpURLConnection uc) throws IOException {
@@ -94,10 +96,11 @@ public abstract class AbuseLimitHandler {
     /**
      * Fail immediately.
      */
+    @Deprecated
     public static final AbuseLimitHandler FAIL = new AbuseLimitHandler() {
         @Override
         public void onError(IOException e, HttpURLConnection uc) throws IOException {
-            throw (IOException) new IOException("Abuse limit reached").initCause(e);
+            throw e;
         }
     };
 }
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index 782aac9d8..064767143 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -113,8 +113,8 @@ public class GitHub {
      */
     GitHub(String apiUrl,
             GitHubConnector connector,
-            RateLimitHandler rateLimitHandler,
-            AbuseLimitHandler abuseLimitHandler,
+            GitHubRateLimitHandler rateLimitHandler,
+            GitHubAbuseLimitHandler abuseLimitHandler,
             GitHubRateLimitChecker rateLimitChecker,
             AuthorizationProvider authorizationProvider) throws IOException {
         if (authorizationProvider instanceof DependentAuthorizationProvider) {
diff --git a/src/main/java/org/kohsuke/github/GitHubAbuseLimitHandler.java b/src/main/java/org/kohsuke/github/GitHubAbuseLimitHandler.java
new file mode 100644
index 000000000..60f2ad185
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubAbuseLimitHandler.java
@@ -0,0 +1,44 @@
+package org.kohsuke.github;
+
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Pluggable strategy to determine what to do when the API rate limit is reached.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GitHubBuilder#withAbuseLimitHandler(AbuseLimitHandler) GitHubBuilder#withRateLimitHandler(AbuseLimitHandler)
+ * @see GitHubRateLimitHandler
+ */
+public abstract class GitHubAbuseLimitHandler extends GitHubConnectorResponseErrorHandler {
+
+    @Override
+    boolean isError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException {
+        return connectorResponse.statusCode() == HttpURLConnection.HTTP_FORBIDDEN
+                && connectorResponse.header("Retry-After") != null;
+    }
+
+    /**
+     * Called when the library encounters HTTP error indicating that the API abuse limit is reached.
+     *
+     * <p>
+     * Any exception thrown from this method will cause the request to fail, and the caller of github-api will receive
+     * an exception. If this method returns normally, another request will be attempted. For that to make sense, the
+     * implementation needs to wait for some time.
+     *
+     * @param connectorResponse
+     *            Response information for this request.
+     * @throws IOException
+     *             on failure
+     * @see <a href="https://developer.github.com/v3/#abuse-rate-limits">API documentation from GitHub</a>
+     * @see <a href=
+     *      "https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits">Dealing
+     *      with abuse rate limits</a>
+     *
+     */
+    public abstract void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException;
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java
index e937f46f1..5afbf207f 100644
--- a/src/main/java/org/kohsuke/github/GitHubBuilder.java
+++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java
@@ -4,6 +4,7 @@ import org.apache.commons.io.IOUtils;
 import org.kohsuke.github.authorization.AuthorizationProvider;
 import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
 import org.kohsuke.github.connector.GitHubConnector;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
 import org.kohsuke.github.extras.ImpatientHttpConnector;
 import org.kohsuke.github.internal.GitHubConnectorHttpConnectorAdapter;
 
@@ -34,8 +35,8 @@ public class GitHubBuilder implements Cloneable {
 
     private GitHubConnector connector;
 
-    private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
-    private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
+    private GitHubRateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
+    private GitHubAbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
     private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker();
     /* private */ AuthorizationProvider authorizationProvider = AuthorizationProvider.ANONYMOUS;
 
@@ -334,6 +335,7 @@ public class GitHubBuilder implements Cloneable {
      *            the connector
      * @return the git hub builder
      */
+    @Deprecated
     public GitHubBuilder withConnector(@Nonnull HttpConnector connector) {
         return withConnector(GitHubConnectorHttpConnectorAdapter.adapt(connector));
     }
@@ -373,6 +375,32 @@ public class GitHubBuilder implements Cloneable {
      * @see #withRateLimitChecker(RateLimitChecker)
      */
     public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) {
+        return withRateLimitHandler((GitHubRateLimitHandler) handler);
+    }
+
+    /**
+     * Adds a {@link GitHubRateLimitHandler} to this {@link GitHubBuilder}.
+     * <p>
+     * GitHub allots a certain number of requests to each user or application per period of time (usually per hour). The
+     * number of requests remaining is returned in the response header and can also be requested using
+     * {@link GitHub#getRateLimit()}. This requests per interval is referred to as the "rate limit".
+     * </p>
+     * <p>
+     * When the remaining number of requests reaches zero, the next request will return an error. If this happens,
+     * {@link GitHubRateLimitHandler#onError(GitHubConnectorResponse)} will be called.
+     * </p>
+     * <p>
+     * NOTE: GitHub treats clients that exceed their rate limit very harshly. If possible, clients should avoid
+     * exceeding their rate limit. Consider adding a {@link RateLimitChecker} to automatically check the rate limit for
+     * each request and wait if needed.
+     * </p>
+     *
+     * @param handler
+     *            the handler
+     * @return the git hub builder
+     * @see #withRateLimitChecker(RateLimitChecker)
+     */
+    public GitHubBuilder withRateLimitHandler(GitHubRateLimitHandler handler) {
         this.rateLimitHandler = handler;
         return this;
     }
@@ -389,7 +417,24 @@ public class GitHubBuilder implements Cloneable {
      *            the handler
      * @return the git hub builder
      */
+    @Deprecated
     public GitHubBuilder withAbuseLimitHandler(AbuseLimitHandler handler) {
+        return withAbuseLimitHandler((GitHubAbuseLimitHandler) handler);
+    }
+
+    /**
+     * Adds a {@link GitHubAbuseLimitHandler} to this {@link GitHubBuilder}.
+     * <p>
+     * When a client sends too many requests in a short time span, GitHub may return an error and set a header telling
+     * the client to not make any more request for some period of time. If this happens,
+     * {@link GitHubAbuseLimitHandler#onError(GitHubConnectorResponse)} will be called.
+     * </p>
+     *
+     * @param handler
+     *            the handler
+     * @return the git hub builder
+     */
+    public GitHubBuilder withAbuseLimitHandler(GitHubAbuseLimitHandler handler) {
         this.abuseLimitHandler = handler;
         return this;
     }
diff --git a/src/main/java/org/kohsuke/github/GitHubClient.java b/src/main/java/org/kohsuke/github/GitHubClient.java
index fffa5f935..feb9ee5d5 100644
--- a/src/main/java/org/kohsuke/github/GitHubClient.java
+++ b/src/main/java/org/kohsuke/github/GitHubClient.java
@@ -25,6 +25,7 @@ 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_BAD_REQUEST;
 import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
 import static java.util.logging.Level.*;
 import static org.apache.commons.lang3.StringUtils.defaultString;
@@ -47,8 +48,8 @@ class GitHubClient {
     // Cache of myself object.
     private final String apiUrl;
 
-    private final RateLimitHandler rateLimitHandler;
-    private final AbuseLimitHandler abuseLimitHandler;
+    private final GitHubRateLimitHandler rateLimitHandler;
+    private final GitHubAbuseLimitHandler abuseLimitHandler;
     private final GitHubRateLimitChecker rateLimitChecker;
     private final AuthorizationProvider authorizationProvider;
 
@@ -74,8 +75,8 @@ class GitHubClient {
 
     GitHubClient(String apiUrl,
             GitHubConnector connector,
-            RateLimitHandler rateLimitHandler,
-            AbuseLimitHandler abuseLimitHandler,
+            GitHubRateLimitHandler rateLimitHandler,
+            GitHubAbuseLimitHandler abuseLimitHandler,
             GitHubRateLimitChecker rateLimitChecker,
             AuthorizationProvider authorizationProvider) throws IOException {
 
@@ -372,47 +373,53 @@ class GitHubClient {
         GitHubConnectorRequest connectorRequest = prepareConnectorRequest(request);
 
         do {
-            // if we fail to create a connection we do not retry and we do not wrap
-
             GitHubConnectorResponse connectorResponse = null;
             try {
-                try {
-                    logRequest(connectorRequest);
-                    rateLimitChecker.checkRateLimit(this, request.rateLimitTarget());
-                    connectorResponse = connector.send(connectorRequest);
-                    noteRateLimit(request.rateLimitTarget(), connectorResponse);
-                    detectOTPRequired(connectorResponse);
-
-                    if (isInvalidCached404Response(connectorResponse)) {
-                        // 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)
-                        connectorRequest = prepareConnectorRequest(
-                                request.toBuilder().setHeader("Cache-Control", "no-cache").build());
-                        continue;
-                    }
-                    if (!(isRateLimitResponse(connectorResponse) || isAbuseLimitResponse(connectorResponse))) {
-                        return createResponse(connectorResponse, handler);
-                    }
-                } catch (IOException e) {
-                    // For transient errors, retry
-                    if (retryConnectionError(e, request.url(), retries)) {
-                        continue;
-                    }
-
-                    throw interpretApiError(e, connectorRequest, connectorResponse);
+                logRequest(connectorRequest);
+                rateLimitChecker.checkRateLimit(this, request.rateLimitTarget());
+                connectorResponse = connector.send(connectorRequest);
+                noteRateLimit(request.rateLimitTarget(), connectorResponse);
+                detectKnownErrors(connectorResponse, request, handler != null);
+                return createResponse(connectorResponse, handler);
+            } catch (RetryRequestException e) {
+                // retry requested by requested by error handler (rate limit handler for example)
+                if (retries > 0 && e.connectorRequest != null) {
+                    connectorRequest = e.connectorRequest;
                 }
-
-                handleLimitingErrors(connectorResponse);
+            } catch (SocketException | SocketTimeoutException | SSLHandshakeException e) {
+                // These transient errors the
+                if (retries > 0) {
+                    logRetryConnectionError(e, request.url(), retries);
+                    continue;
+                }
+                throw interpretApiError(e, connectorRequest, connectorResponse);
+            } catch (IOException e) {
+                throw interpretApiError(e, connectorRequest, connectorResponse);
             } finally {
                 IOUtils.closeQuietly(connectorResponse);
             }
-
         } while (--retries >= 0);
 
         throw new GHIOException("Ran out of retries for URL: " + request.url().toString());
     }
 
+    private void detectKnownErrors(GitHubConnectorResponse connectorResponse,
+            GitHubRequest request,
+            boolean detectStatusCodeError) throws IOException {
+        detectOTPRequired(connectorResponse);
+        detectInvalidCached404Response(connectorResponse, request);
+        if (rateLimitHandler.isError(connectorResponse)) {
+            rateLimitHandler.onError(connectorResponse);
+            throw new RetryRequestException();
+        } else if (abuseLimitHandler.isError(connectorResponse)) {
+            abuseLimitHandler.onError(connectorResponse);
+            throw new RetryRequestException();
+        } else if (detectStatusCodeError
+                && GitHubConnectorResponseErrorHandler.STATUS_HTTP_BAD_REQUEST_OR_GREATER.isError(connectorResponse)) {
+            GitHubConnectorResponseErrorHandler.STATUS_HTTP_BAD_REQUEST_OR_GREATER.onError(connectorResponse);
+        }
+    }
+
     private GitHubConnectorRequest prepareConnectorRequest(GitHubRequest request) throws IOException {
         GitHubRequest.Builder<?> builder = request.toBuilder();
         // if the authentication is needed but no credential is given, try it anyway (so that some calls
@@ -451,20 +458,22 @@ class GitHubClient {
                         + request.method() + " " + request.url().toString());
     }
 
-    private void handleLimitingErrors(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException {
-        if (isRateLimitResponse(connectorResponse)) {
-            rateLimitHandler.onError(connectorResponse);
-        } else if (isAbuseLimitResponse(connectorResponse)) {
-            abuseLimitHandler.onError(connectorResponse);
-        }
-    }
-
     @Nonnull
     private static <T> GitHubResponse<T> createResponse(@Nonnull GitHubConnectorResponse connectorResponse,
             @CheckForNull BodyHandler<T> handler) throws IOException {
         T body = null;
+        if (handler != null) {
+            if (!shouldIgnoreBody(connectorResponse)) {
+                body = handler.apply(connectorResponse);
+            }
+        }
+        return new GitHubResponse<>(connectorResponse, body);
+    }
+
+    private static boolean shouldIgnoreBody(@Nonnull GitHubConnectorResponse connectorResponse) {
         if (connectorResponse.statusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
             // special case handling for 304 unmodified, as the content will be ""
+            return true;
         } else if (connectorResponse.statusCode() == HttpURLConnection.HTTP_ACCEPTED) {
 
             // Response code 202 means data is being generated or an action that can require some time is triggered.
@@ -476,10 +485,10 @@ class GitHubClient {
             LOGGER.log(FINE,
                     "Received HTTP_ACCEPTED(202) from " + connectorResponse.request().url().toString()
                             + " . Please try again in 5 seconds.");
-        } else if (handler != null) {
-            body = handler.apply(connectorResponse);
+            return true;
+        } else {
+            return false;
         }
-        return new GitHubResponse<>(connectorResponse, body);
     }
 
     /**
@@ -502,7 +511,9 @@ class GitHubClient {
             statusCode = connectorResponse.statusCode();
             message = connectorResponse.header("Status");
             headers = connectorResponse.allHeaders();
-            errorMessage = connectorResponse.errorMessage();
+            if (connectorResponse.statusCode() >= HTTP_BAD_REQUEST) {
+                errorMessage = GitHubResponse.getBodyAsStringOrNull(connectorResponse);
+            }
         }
 
         if (errorMessage != null) {
@@ -521,35 +532,20 @@ class GitHubClient {
         return e;
     }
 
-    protected static boolean isRateLimitResponse(@Nonnull GitHubConnectorResponse connectorResponse) {
-        return connectorResponse.statusCode() == HttpURLConnection.HTTP_FORBIDDEN
-                && "0".equals(connectorResponse.header("X-RateLimit-Remaining"));
-    }
-
-    protected static boolean isAbuseLimitResponse(@Nonnull GitHubConnectorResponse connectorResponse) {
-        return connectorResponse.statusCode() == HttpURLConnection.HTTP_FORBIDDEN
-                && connectorResponse.header("Retry-After") != null;
-    }
-
-    private static boolean retryConnectionError(IOException e, URL url, int retries) throws IOException {
+    private static void logRetryConnectionError(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
-        boolean connectionError = e instanceof SocketException || e instanceof SocketTimeoutException
-                || e instanceof SSLHandshakeException;
-        if (connectionError && retries > 0) {
-            LOGGER.log(INFO,
-                    e.getMessage() + " while connecting to " + url + ". Sleeping " + GitHubClient.retryTimeoutMillis
-                            + " milliseconds before retrying... ; will try " + retries + " more time(s)");
-            try {
-                Thread.sleep(GitHubClient.retryTimeoutMillis);
-            } catch (InterruptedException ie) {
-                throw (IOException) new InterruptedIOException().initCause(e);
-            }
-            return true;
+        LOGGER.log(INFO,
+                e.getMessage() + " while connecting to " + url + ". Sleeping " + GitHubClient.retryTimeoutMillis
+                        + " milliseconds before retrying... ; will try " + retries + " more time(s)");
+        try {
+            Thread.sleep(GitHubClient.retryTimeoutMillis);
+        } catch (InterruptedException ie) {
+            throw (IOException) new InterruptedIOException().initCause(e);
         }
-        return false;
     }
 
-    private static boolean isInvalidCached404Response(GitHubConnectorResponse connectorResponse) {
+    private void detectInvalidCached404Response(GitHubConnectorResponse connectorResponse, GitHubRequest request)
+            throws IOException {
         // WORKAROUND FOR ISSUE #669:
         // When the Requester detects a 404 response with an ETag (only happens when the server's 304
         // is bogus and would cause cache corruption), try the query again with new request header
@@ -566,9 +562,12 @@ class GitHubClient {
             LOGGER.log(FINE,
                     "Encountered GitHub invalid cached 404 from " + connectorResponse.request().url()
                             + ". Retrying with \"Cache-Control\"=\"no-cache\"...");
-            return true;
+            // 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)
+            throw new RetryRequestException(
+                    prepareConnectorRequest(request.toBuilder().setHeader("Cache-Control", "no-cache").build()));
         }
-        return false;
     }
 
     private void noteRateLimit(@Nonnull RateLimitTarget rateLimitTarget,
@@ -754,4 +753,15 @@ class GitHubClient {
     static <T> List<T> unmodifiableListOrNull(List<? extends T> list) {
         return list == null ? null : Collections.unmodifiableList(list);
     }
+
+    static class RetryRequestException extends IOException {
+        final GitHubConnectorRequest connectorRequest;
+        RetryRequestException() {
+            this(null);
+        }
+
+        RetryRequestException(GitHubConnectorRequest connectorRequest) {
+            this.connectorRequest = connectorRequest;
+        }
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubConnectorResponseErrorHandler.java b/src/main/java/org/kohsuke/github/GitHubConnectorResponseErrorHandler.java
new file mode 100644
index 000000000..ad69abea7
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubConnectorResponseErrorHandler.java
@@ -0,0 +1,61 @@
+package org.kohsuke.github;
+
+import org.jetbrains.annotations.NotNull;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+
+/**
+ * Pluggable strategy to determine what to detect and choose what to do when various errors occur during an http
+ * request.
+ *
+ * @author Liam Newman
+ */
+abstract class GitHubConnectorResponseErrorHandler {
+
+    /**
+     * Called to detect an error handled by this handler.
+     *
+     * @return {@code true} if there is an error and {@link #onError(GitHubConnectorResponse)} should be called
+     */
+    abstract boolean isError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException;
+
+    /**
+     * Called when the library encounters HTTP error matching {@link #isError(GitHubConnectorResponse)}
+     *
+     * <p>
+     * Any exception thrown from this method will cause the request to fail, and the caller of github-api will receive
+     * an exception. If this method returns normally, another request will be attempted. For that to make sense, the
+     * implementation needs to wait for some time.
+     *
+     * @param connectorResponse
+     *            Response information for this request.
+     *
+     * @throws IOException
+     *             the io exception
+     * @see <a href="https://developer.github.com/v3/#rate-limiting">API documentation from GitHub</a>
+     */
+    public abstract void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException;
+
+    static GitHubConnectorResponseErrorHandler STATUS_HTTP_BAD_REQUEST_OR_GREATER = new GitHubConnectorResponseErrorHandler() {
+        @Override
+        public boolean isError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
+            return connectorResponse.statusCode() >= HTTP_BAD_REQUEST;
+        }
+
+        @Override
+        public void onError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
+            if (connectorResponse.statusCode() == HTTP_NOT_FOUND) {
+                throw new FileNotFoundException(connectorResponse.request().url().toString());
+            } else {
+                throw new HttpException(connectorResponse);
+            }
+        }
+    };
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubRateLimitHandler.java b/src/main/java/org/kohsuke/github/GitHubRateLimitHandler.java
new file mode 100644
index 000000000..ec977a1f3
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubRateLimitHandler.java
@@ -0,0 +1,42 @@
+package org.kohsuke.github;
+
+import org.jetbrains.annotations.NotNull;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Pluggable strategy to determine what to do when the API rate limit is reached.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GitHubBuilder#withRateLimitHandler(RateLimitHandler) GitHubBuilder#withRateLimitHandler(RateLimitHandler)
+ * @see GitHubAbuseLimitHandler
+ */
+public abstract class GitHubRateLimitHandler extends GitHubConnectorResponseErrorHandler {
+
+    @Override
+    boolean isError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
+        return connectorResponse.statusCode() == HttpURLConnection.HTTP_FORBIDDEN
+                && "0".equals(connectorResponse.header("X-RateLimit-Remaining"));
+    }
+
+    /**
+     * Called when the library encounters HTTP error indicating that the API rate limit has been exceeded.
+     *
+     * <p>
+     * Any exception thrown from this method will cause the request to fail, and the caller of github-api will receive
+     * an exception. If this method returns normally, another request will be attempted. For that to make sense, the
+     * implementation needs to wait for some time.
+     *
+     * @param connectorResponse
+     *            Response information for this request.
+     *
+     * @throws IOException
+     *             the io exception
+     * @see <a href="https://developer.github.com/v3/#rate-limiting">API documentation from GitHub</a>
+     */
+    public abstract void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException;
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubResponse.java b/src/main/java/org/kohsuke/github/GitHubResponse.java
index fdafa1dd0..1cbda58ee 100644
--- a/src/main/java/org/kohsuke/github/GitHubResponse.java
+++ b/src/main/java/org/kohsuke/github/GitHubResponse.java
@@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils;
 import org.kohsuke.github.connector.GitHubConnectorResponse;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.lang.reflect.Array;
 import java.net.HttpURLConnection;
@@ -126,9 +127,26 @@ class GitHubResponse<T> {
      */
     @Nonnull
     static String getBodyAsString(GitHubConnectorResponse connectorResponse) throws IOException {
-        InputStreamReader r = null;
-        r = new InputStreamReader(connectorResponse.bodyStream(), StandardCharsets.UTF_8);
-        return IOUtils.toString(r);
+        InputStream inputStream = connectorResponse.bodyStream();
+        try (InputStreamReader r = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
+            return IOUtils.toString(r);
+        }
+    }
+
+    /**
+     * Gets the body of the response as a {@link String}.
+     *
+     * @return the body of the response as a {@link String}.
+     * @throws IOException
+     *             if an I/O Exception occurs.
+     * @param connectorResponse
+     */
+    static String getBodyAsStringOrNull(GitHubConnectorResponse connectorResponse) {
+        try {
+            return getBodyAsString(connectorResponse);
+        } catch (NullPointerException | IOException e) {
+        }
+        return null;
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/HttpException.java b/src/main/java/org/kohsuke/github/HttpException.java
index c3dda8887..f1bbd38f6 100644
--- a/src/main/java/org/kohsuke/github/HttpException.java
+++ b/src/main/java/org/kohsuke/github/HttpException.java
@@ -1,5 +1,7 @@
 package org.kohsuke.github;
 
+import org.kohsuke.github.connector.GitHubConnectorResponse;
+
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URL;
@@ -106,6 +108,20 @@ public class HttpException extends GHIOException {
         this(responseCode, responseMessage, url == null ? null : url.toString(), cause);
     }
 
+    /**
+     * Instantiates a new Http exception.
+     *
+     * @param connectorResponse
+     *            the connector response to base this on
+     */
+    public HttpException(GitHubConnectorResponse connectorResponse) {
+        this(GitHubResponse.getBodyAsStringOrNull(connectorResponse),
+                connectorResponse.statusCode(),
+                connectorResponse.header("Status"),
+                connectorResponse.request().url().toString());
+        this.responseHeaderFields = connectorResponse.allHeaders();
+    }
+
     /**
      * Http response code of the request that cause the exception
      *
diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java
index ce7e7be6c..d5449dda8 100644
--- a/src/main/java/org/kohsuke/github/RateLimitHandler.java
+++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java
@@ -12,10 +12,12 @@ import javax.annotation.Nonnull;
  * Pluggable strategy to determine what to do when the API rate limit is reached.
  *
  * @author Kohsuke Kawaguchi
- * @see GitHubBuilder#withRateLimitHandler(RateLimitHandler) GitHubBuilder#withRateLimitHandler(RateLimitHandler)
+ * @see GitHubBuilder#withRateLimitHandler(GitHubRateLimitHandler)
+ *      GitHubBuilder#withRateLimitHandler(GitHubRateLimitHandler)
  * @see AbuseLimitHandler
  */
-public abstract class RateLimitHandler {
+@Deprecated
+public abstract class RateLimitHandler extends GitHubRateLimitHandler {
 
     /**
      * Called when the library encounters HTTP error indicating that the API rate limit has been exceeded.
@@ -33,7 +35,7 @@ public abstract class RateLimitHandler {
      * @see <a href="https://developer.github.com/v3/#rate-limiting">API documentation from GitHub</a>
      */
     public void onError(@Nonnull GitHubConnectorResponse connectorResponse) throws IOException {
-        GHIOException e = new HttpException("Rate limit violation",
+        GHIOException e = new HttpException("API rate limit reached",
                 connectorResponse.statusCode(),
                 connectorResponse.header("Status"),
                 connectorResponse.request().url().toString()).withResponseHeaderFields(connectorResponse.allHeaders());
@@ -58,12 +60,12 @@ public abstract class RateLimitHandler {
      * @see <a href="https://developer.github.com/v3/#rate-limiting">API documentation from GitHub</a>
      */
     @Deprecated
-    public void onError(IOException e, HttpURLConnection uc) throws IOException {
-    }
+    public abstract void onError(IOException e, HttpURLConnection uc) throws IOException;
 
     /**
      * Block until the API rate limit is reset. Useful for long-running batch processing.
      */
+    @Deprecated
     public static final RateLimitHandler WAIT = new RateLimitHandler() {
         @Override
         public void onError(IOException e, HttpURLConnection uc) throws IOException {
@@ -86,10 +88,11 @@ public abstract class RateLimitHandler {
     /**
      * Fail immediately.
      */
+    @Deprecated
     public static final RateLimitHandler FAIL = new RateLimitHandler() {
         @Override
         public void onError(IOException e, HttpURLConnection uc) throws IOException {
-            throw (IOException) new IOException("API rate limit reached").initCause(e);
+            throw e;
         }
     };
 }
diff --git a/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java
index dedc8495f..75595727e 100644
--- a/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java
+++ b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponse.java
@@ -48,6 +48,7 @@ public abstract class GitHubConnectorResponse implements Closeable {
      * Get this response as a {@link HttpURLConnection}.
      *
      * @return an object that implements at least the response related methods of {@link HttpURLConnection}.
+     * @deprecated This method is present only to provide backward compatibility with other deprecated components.
      */
     @Deprecated
     @Nonnull
@@ -82,13 +83,6 @@ public abstract class GitHubConnectorResponse implements Closeable {
      */
     public abstract InputStream bodyStream() throws IOException;
 
-    /**
-     * The error message for this response.
-     *
-     * @return if there is an error with some error string, that is returned. If not, {@code null}.
-     */
-    public abstract String errorMessage();
-
     /**
      * Gets the {@link GitHubConnectorRequest} for this response.
      *
@@ -119,4 +113,25 @@ public abstract class GitHubConnectorResponse implements Closeable {
         return headers;
     }
 
+    /**
+     * Unwraps a {@link GitHubConnectorResponse} from a {@link HttpURLConnection} adapter.
+     *
+     * Only works on the internal {@link GitHubConnectorResponseHttpUrlConnectionAdapter}.
+     *
+     * @param connection
+     *            the connection to unwrap.
+     * @return an unwrapped response from an adapter.
+     * @throws UnsupportedOperationException
+     *             if the connection is not an adapter.
+     * @deprecated Only preset for testing and interaction with deprecated HttpURLConnection components.
+     */
+    @Deprecated
+    public final static GitHubConnectorResponse fromHttpURLConnectionAdapter(HttpURLConnection connection) {
+        if (connection instanceof GitHubConnectorResponseHttpUrlConnectionAdapter) {
+            return ((GitHubConnectorResponseHttpUrlConnectionAdapter) connection).connectorResponse();
+        } else {
+            throw new UnsupportedOperationException(
+                    "Cannot unwrap GitHubConnectorResponse from " + connection.getClass().getName());
+        }
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponseHttpUrlConnectionAdapter.java b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponseHttpUrlConnectionAdapter.java
index 1b4b0d639..4ae6b83cb 100644
--- a/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponseHttpUrlConnectionAdapter.java
+++ b/src/main/java/org/kohsuke/github/connector/GitHubConnectorResponseHttpUrlConnectionAdapter.java
@@ -1,14 +1,22 @@
 package org.kohsuke.github.connector;
 
-import java.io.ByteArrayInputStream;
+import org.kohsuke.github.HttpException;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
-import java.nio.charset.StandardCharsets;
 import java.security.Permission;
 import java.util.*;
 
+/**
+ * Adapter class for {@link org.kohsuke.github.connector.GitHubConnectorResponse} to be usable as a
+ * {@link HttpURLConnection}.
+ *
+ * Behavior is equivalent to a {@link HttpURLConnection} after {@link HttpURLConnection#connect()} has been called.
+ * Methods that make no sense throw {@link UnsupportedOperationException}.
+ */
+@Deprecated
 class GitHubConnectorResponseHttpUrlConnectionAdapter extends HttpURLConnection {
 
     private final GitHubConnectorResponse connectorResponse;
@@ -19,6 +27,16 @@ class GitHubConnectorResponseHttpUrlConnectionAdapter extends HttpURLConnection
         this.connectorResponse = connectorResponse;
     }
 
+    /**
+     * Readable to support {@link GitHubConnectorResponse#fromHttpURLConnectionAdapter(HttpURLConnection)} which only
+     * exist for testing.
+     *
+     * @return
+     */
+    GitHubConnectorResponse connectorResponse() {
+        return connectorResponse;
+    }
+
     @Override
     public String getHeaderFieldKey(int n) {
         List<String> keys = new ArrayList<>(connectorResponse.allHeaders().keySet());
@@ -72,7 +90,13 @@ class GitHubConnectorResponseHttpUrlConnectionAdapter extends HttpURLConnection
 
     @Override
     public InputStream getErrorStream() {
-        return new ByteArrayInputStream(connectorResponse.errorMessage().getBytes(StandardCharsets.UTF_8));
+        try {
+            if (connectorResponse.statusCode() >= HTTP_BAD_REQUEST) {
+                return connectorResponse.bodyStream();
+            }
+        } catch (IOException e) {
+        }
+        return null;
     }
 
     @Override
@@ -175,6 +199,10 @@ class GitHubConnectorResponseHttpUrlConnectionAdapter extends HttpURLConnection
 
     @Override
     public InputStream getInputStream() throws IOException {
+        // This should only be possible in abuse or rate limit scenario
+        if (connectorResponse.statusCode() >= HTTP_BAD_REQUEST) {
+            throw new HttpException(connectorResponse);
+        }
         return connectorResponse.bodyStream();
     }
 
diff --git a/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpGitHubConnector.java b/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpGitHubConnector.java
index f4d4e9688..5394e66f1 100644
--- a/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpGitHubConnector.java
+++ b/src/main/java/org/kohsuke/github/extras/okhttp3/OkHttpGitHubConnector.java
@@ -8,10 +8,8 @@ import org.kohsuke.github.connector.GitHubConnectorRequest;
 import org.kohsuke.github.connector.GitHubConnectorResponse;
 
 import java.io.ByteArrayInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -21,10 +19,6 @@ import java.util.zip.GZIPInputStream;
 
 import javax.annotation.Nonnull;
 
-import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
-import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
-import static java.util.logging.Level.FINER;
-
 /**
  * {@link HttpConnector} for {@link OkHttpClient}.
  * <p>
@@ -129,17 +123,6 @@ public class OkHttpGitHubConnector implements GitHubConnector {
          * {@inheritDoc}
          */
         public InputStream bodyStream() throws IOException {
-            if (response.code() >= HTTP_BAD_REQUEST) {
-                if (response.code() == HTTP_NOT_FOUND) {
-                    throw new FileNotFoundException(request().url().toString());
-                } else {
-                    throw new HttpException(errorMessage(),
-                            response.code(),
-                            response.message(),
-                            request().url().toString());
-                }
-            }
-
             readBodyBytes();
             InputStream stream = bodyBytes == null ? null : new ByteArrayInputStream(bodyBytes);
             return stream;
@@ -159,24 +142,7 @@ public class OkHttpGitHubConnector implements GitHubConnector {
                     }
                     bodyBytesRead = true;
                 }
-
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        public String errorMessage() {
-            String result = null;
-            try {
-                if (!response.isSuccessful()) {
-                    readBodyBytes();
-                    result = bodyBytes == null ? null : new String(bodyBytes, StandardCharsets.UTF_8);
-                }
-            } catch (Exception e) {
-                LOGGER.log(FINER, "Ignored exception get error message", e);
             }
-            return result;
         }
 
         /**
diff --git a/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java b/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
index 54c736afd..b4a55dab1 100644
--- a/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
+++ b/src/main/java/org/kohsuke/github/internal/GitHubConnectorHttpConnectorAdapter.java
@@ -8,14 +8,12 @@ import org.kohsuke.github.connector.GitHubConnectorRequest;
 import org.kohsuke.github.connector.GitHubConnectorResponse;
 
 import java.io.ByteArrayInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.net.HttpURLConnection;
 import java.net.ProtocolException;
 import java.net.URL;
-import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
 import java.util.logging.Logger;
@@ -23,8 +21,6 @@ import java.util.zip.GZIPInputStream;
 
 import javax.annotation.Nonnull;
 
-import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
-import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
 import static java.util.logging.Level.*;
 
 /**
@@ -163,8 +159,6 @@ public final class GitHubConnectorHttpConnectorAdapter implements GitHubConnecto
 
         private boolean inputStreamRead = false;
         private byte[] inputBytes = null;
-        private boolean errorStreamRead = false;
-        private String errorString = null;
 
         @Nonnull
         private final HttpURLConnection connection;
@@ -181,20 +175,13 @@ public final class GitHubConnectorHttpConnectorAdapter implements GitHubConnecto
          * {@inheritDoc}
          */
         public InputStream bodyStream() throws IOException {
-            if (connection.getResponseCode() >= HTTP_BAD_REQUEST) {
-                if (connection.getResponseCode() == HTTP_NOT_FOUND) {
-                    throw new FileNotFoundException(request().url().toString());
-                } else {
-                    throw new HttpException(errorMessage(),
-                            connection.getResponseCode(),
-                            connection.getResponseMessage(),
-                            connection.getURL().toString());
-                }
-            }
-
             synchronized (this) {
                 if (!inputStreamRead) {
-                    try (InputStream stream = wrapStream(connection.getInputStream())) {
+                    InputStream rawStream = connection.getErrorStream();
+                    if (rawStream == null) {
+                        rawStream = connection.getInputStream();
+                    }
+                    try (InputStream stream = wrapStream(rawStream)) {
                         if (stream != null) {
                             inputBytes = IOUtils.toByteArray(stream);
                         }
@@ -209,28 +196,6 @@ public final class GitHubConnectorHttpConnectorAdapter implements GitHubConnecto
         /**
          * {@inheritDoc}
          */
-        public String errorMessage() {
-            String result = null;
-            try {
-                synchronized (this) {
-                    if (!errorStreamRead) {
-                        try (InputStream stream = wrapStream(connection.getErrorStream())) {
-                            if (stream != null) {
-                                errorString = new String(IOUtils.toByteArray(stream), StandardCharsets.UTF_8);
-                            }
-                        }
-                        errorStreamRead = true;
-                    }
-                }
-                if (errorString != null) {
-                    result = errorString;
-                }
-            } catch (Exception e) {
-                LOGGER.log(FINER, "Ignored exception get error message", e);
-            }
-            return result;
-        }
-
         @SuppressFBWarnings(value = { "EI_EXPOSE_REP" },
                 justification = "Internal implementation class. Should not be used externally.")
         @Nonnull


Test patch:
diff --git a/src/test/java/org/kohsuke/github/AbuseLimitHandlerTest.java b/src/test/java/org/kohsuke/github/AbuseLimitHandlerTest.java
index 694a44faa..c09d15d18 100644
--- a/src/test/java/org/kohsuke/github/AbuseLimitHandlerTest.java
+++ b/src/test/java/org/kohsuke/github/AbuseLimitHandlerTest.java
@@ -5,6 +5,7 @@ import org.apache.commons.io.IOUtils;
 import org.hamcrest.Matchers;
 import org.junit.Assert;
 import org.junit.Test;
+import org.kohsuke.github.connector.GitHubConnectorResponse;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -59,6 +60,13 @@ public class AbuseLimitHandlerTest extends AbstractGitHubWireMockTest {
                     @Override
                     public void onError(IOException e, HttpURLConnection uc) throws IOException {
 
+                        GitHubConnectorResponse connectorResponse = null;
+                        try {
+                            connectorResponse = GitHubConnectorResponse.fromHttpURLConnectionAdapter(uc);
+                        } catch (UnsupportedOperationException ex) {
+                            assertThat(ex.getMessage(), startsWith("Cannot unwrap GitHubConnectorResponse"));
+                        }
+
                         // Verify
                         assertThat(uc.getDate(), Matchers.greaterThanOrEqualTo(new Date().getTime() - 10000));
                         assertThat(uc.getExpiration(), equalTo(0L));
@@ -87,6 +95,12 @@ public class AbuseLimitHandlerTest extends AbstractGitHubWireMockTest {
                         String error = IOUtils.toString(errorStream, StandardCharsets.UTF_8);
                         assertThat(error, containsString("Must have push access to repository"));
 
+                        if (connectorResponse != null) {
+                            String connectorBody = IOUtils.toString(connectorResponse.bodyStream(),
+                                    StandardCharsets.UTF_8);
+                            assertThat(connectorBody, containsString("Must have push access to repository"));
+                        }
+
                         // calling again should still error
                         ioEx = Assert.assertThrows(IOException.class, () -> uc.getInputStream());
 
@@ -116,12 +130,10 @@ public class AbuseLimitHandlerTest extends AbstractGitHubWireMockTest {
                         Assert.assertThrows(IllegalStateException.class, () -> uc.setRequestProperty("bogus", "thing"));
                         Assert.assertThrows(IllegalStateException.class, () -> uc.setUseCaches(true));
 
-                        boolean isAdapter = false;
-                        if (uc.toString().contains("GitHubConnectorResponseHttpUrlConnectionAdapter")) {
-                            isAdapter = true;
-                        }
+                        if (connectorResponse != null) {
+                            assertThat(uc.toString(),
+                                    containsString("GitHubConnectorResponseHttpUrlConnectionAdapter"));
 
-                        if (isAdapter) {
                             Assert.assertThrows(UnsupportedOperationException.class,
                                     () -> uc.getAllowUserInteraction());
                             Assert.assertThrows(UnsupportedOperationException.class, () -> uc.getConnectTimeout());
@@ -176,8 +188,8 @@ public class AbuseLimitHandlerTest extends AbstractGitHubWireMockTest {
             getTempRepository();
             fail();
         } catch (Exception e) {
-            assertThat(e, instanceOf(IOException.class));
-            assertThat(e.getCause(), instanceOf(HttpException.class));
+            assertThat(e, instanceOf(HttpException.class));
+            assertThat(e.getMessage(), equalTo("Abuse limit reached"));
         }
 
         assertThat(mockGitHub.getRequestCount(), equalTo(2));
@@ -203,8 +215,8 @@ public class AbuseLimitHandlerTest extends AbstractGitHubWireMockTest {
 
             fail();
         } catch (Exception e) {
-            assertThat(e, instanceOf(IOException.class));
-            assertThat(e.getCause(), instanceOf(HttpException.class));
+            assertThat(e, instanceOf(HttpException.class));
+            assertThat(e.getMessage(), equalTo("Abuse limit reached"));
         }
 
         assertThat(mockGitHub.getRequestCount(), equalTo(2));
diff --git a/src/test/java/org/kohsuke/github/RateLimitHandlerTest.java b/src/test/java/org/kohsuke/github/RateLimitHandlerTest.java
index ea2ad5a89..7f304e7fb 100644
--- a/src/test/java/org/kohsuke/github/RateLimitHandlerTest.java
+++ b/src/test/java/org/kohsuke/github/RateLimitHandlerTest.java
@@ -56,8 +56,8 @@ public class RateLimitHandlerTest extends AbstractGitHubWireMockTest {
             getTempRepository();
             fail();
         } catch (Exception e) {
-            assertThat(e, instanceOf(IOException.class));
-            assertThat(e.getCause(), instanceOf(HttpException.class));
+            assertThat(e, instanceOf(HttpException.class));
+            assertThat(e.getMessage(), equalTo("API rate limit reached"));
         }
 
         assertThat(mockGitHub.getRequestCount(), equalTo(2));
@@ -83,8 +83,8 @@ public class RateLimitHandlerTest extends AbstractGitHubWireMockTest {
 
             fail();
         } catch (Exception e) {
-            assertThat(e, instanceOf(IOException.class));
-            assertThat(e.getCause(), instanceOf(HttpException.class));
+            assertThat(e, instanceOf(HttpException.class));
+            assertThat(e.getMessage(), equalTo("API rate limit reached"));
         }
 
         assertThat(mockGitHub.getRequestCount(), equalTo(2));