Skip to content

Commit af19581

Browse files
Implement ServiceDownException for case GitHub's API is down (#1813)
* Implement ServiceDownException for case GitHub's API is down * Update src/main/java/org/kohsuke/github/GitHubConnectorResponseErrorHandler.java * Update src/main/java/org/kohsuke/github/GitHubConnectorResponseErrorHandler.java * Apply suggestions from code review * Update src/main/java/org/kohsuke/github/ServiceDownException.java --------- Co-authored-by: Liam Newman <[email protected]>
1 parent eb269bd commit af19581

File tree

8 files changed

+620
-0
lines changed

8 files changed

+620
-0
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import org.jetbrains.annotations.NotNull;
44
import org.kohsuke.github.connector.GitHubConnectorResponse;
55

6+
import java.io.BufferedReader;
67
import java.io.FileNotFoundException;
78
import java.io.IOException;
9+
import java.io.InputStreamReader;
10+
import java.nio.charset.StandardCharsets;
811

912
import javax.annotation.Nonnull;
1013

1114
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
15+
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
1216
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
1317

1418
// TODO: Auto-generated Javadoc
@@ -49,6 +53,10 @@ abstract class GitHubConnectorResponseErrorHandler {
4953

5054
/** The status http bad request or greater. */
5155
static GitHubConnectorResponseErrorHandler STATUS_HTTP_BAD_REQUEST_OR_GREATER = new GitHubConnectorResponseErrorHandler() {
56+
private static final String CONTENT_TYPE = "Content-type";
57+
private static final String TEXT_HTML = "text/html";
58+
private static final String UNICORN_TITLE = "<title>Unicorn!";
59+
5260
@Override
5361
public boolean isError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
5462
return connectorResponse.statusCode() >= HTTP_BAD_REQUEST;
@@ -58,9 +66,38 @@ public boolean isError(@NotNull GitHubConnectorResponse connectorResponse) throw
5866
public void onError(@NotNull GitHubConnectorResponse connectorResponse) throws IOException {
5967
if (connectorResponse.statusCode() == HTTP_NOT_FOUND) {
6068
throw new FileNotFoundException(connectorResponse.request().url().toString());
69+
} else if (isServiceDown(connectorResponse)) {
70+
throw new ServiceDownException(connectorResponse);
6171
} else {
6272
throw new HttpException(connectorResponse);
6373
}
6474
}
75+
76+
private boolean isServiceDown(GitHubConnectorResponse connectorResponse) throws IOException {
77+
if (connectorResponse.statusCode() < HTTP_INTERNAL_ERROR) {
78+
return false;
79+
}
80+
81+
String contentTypeHeader = connectorResponse.header(CONTENT_TYPE);
82+
if (contentTypeHeader != null && contentTypeHeader.contains(TEXT_HTML)) {
83+
try (BufferedReader bufReader = new BufferedReader(
84+
new InputStreamReader(connectorResponse.bodyStream(), StandardCharsets.UTF_8))) {
85+
String line;
86+
int hardLineCap = 25;
87+
// <title> node is expected in the beginning anyway.
88+
// This way we do not load the raw long images' Strings, which are later in the HTML code
89+
// Regex or .contains would result in iterating the whole HTML document, if it didn't match
90+
// UNICORN_TITLE
91+
while (hardLineCap > 0 && (line = bufReader.readLine()) != null) {
92+
if (line.trim().startsWith(UNICORN_TITLE)) {
93+
return true;
94+
}
95+
hardLineCap--;
96+
}
97+
}
98+
}
99+
100+
return false;
101+
}
65102
};
66103
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.kohsuke.github;
2+
3+
import org.kohsuke.github.connector.GitHubConnectorResponse;
4+
5+
import java.io.IOException;
6+
7+
/**
8+
* Special {@link IOException} case for http exceptions, when {@link HttpException} is thrown due to GitHub service
9+
* being down.
10+
*
11+
* Inherits from {@link HttpException} to maintain compatibility with existing clients.
12+
*
13+
* @author <a href="mailto:[email protected]">Rastislav Budinsky</a>
14+
*/
15+
public class ServiceDownException extends HttpException {
16+
17+
/**
18+
* Instantiates a new service down exception.
19+
*
20+
* @param connectorResponse
21+
* the connector response to base this on
22+
*/
23+
public ServiceDownException(GitHubConnectorResponse connectorResponse) {
24+
super(connectorResponse);
25+
}
26+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,20 @@ public void testHeaderFieldName() throws Exception {
397397
org.getResponseHeaderFields().keySet().contains("CacHe-ControL"));
398398
assertThat(org.getResponseHeaderFields().get("cachE-cOntrol").get(0), is("private, max-age=60, s-maxage=60"));
399399
}
400+
401+
/**
402+
* Test expect GitHub {@link ServiceDownException}
403+
*
404+
*/
405+
@Test
406+
public void testCatchServiceDownException() {
407+
snapshotNotAllowed();
408+
try {
409+
GHRepository repo = gitHub.getRepository("hub4j-test-org/github-api");
410+
repo.getFileContent("ghcontent-ro/service-down");
411+
fail("Exception was expected");
412+
} catch (IOException e) {
413+
assertThat(e.getClass().getName(), equalToIgnoringCase(ServiceDownException.class.getName()));
414+
}
415+
}
400416
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"login": "The-Huginn",
3+
"id": 78657734,
4+
"node_id": "MDQ6VXNlcjc4NjU3NzM0",
5+
"avatar_url": "https://avatars.githubusercontent.com/u/78657734?v=4",
6+
"gravatar_id": "",
7+
"url": "https://api.github.com/users/The-Huginn",
8+
"html_url": "https://github.com/The-Huginn",
9+
"followers_url": "https://api.github.com/users/The-Huginn/followers",
10+
"following_url": "https://api.github.com/users/The-Huginn/following{/other_user}",
11+
"gists_url": "https://api.github.com/users/The-Huginn/gists{/gist_id}",
12+
"starred_url": "https://api.github.com/users/The-Huginn/starred{/owner}{/repo}",
13+
"subscriptions_url": "https://api.github.com/users/The-Huginn/subscriptions",
14+
"organizations_url": "https://api.github.com/users/The-Huginn/orgs",
15+
"repos_url": "https://api.github.com/users/The-Huginn/repos",
16+
"events_url": "https://api.github.com/users/The-Huginn/events{/privacy}",
17+
"received_events_url": "https://api.github.com/users/The-Huginn/received_events",
18+
"type": "User",
19+
"site_admin": false,
20+
"name": "Rastislav Budinsky",
21+
"company": "Red Hat",
22+
"blog": "thehuginn.com",
23+
"location": null,
24+
"email": null,
25+
"hireable": null,
26+
"bio": null,
27+
"twitter_username": "The_Hug1nn",
28+
"public_repos": 47,
29+
"public_gists": 0,
30+
"followers": 3,
31+
"following": 2,
32+
"created_at": "2021-02-06T17:33:36Z",
33+
"updated_at": "2024-03-11T08:56:45Z"
34+
}

0 commit comments

Comments
 (0)