Skip to content

Commit a74ccc1

Browse files
committed
Added core error handling abstractions
1 parent b269191 commit a74ccc1

File tree

7 files changed

+474
-3
lines changed

7 files changed

+474
-3
lines changed

src/main/java/com/google/firebase/IncomingHttpResponse.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,29 @@ public final class IncomingHttpResponse {
3535
private final Map<String, Object> headers;
3636
private final OutgoingHttpRequest request;
3737

38-
IncomingHttpResponse(HttpResponse response, @Nullable String content) {
38+
/**
39+
* Creates an IncomingHttpResponse from a successful HttpResponse and the content read from it.
40+
* The caller is expected to read the content from the response, and handle any errors that
41+
* may occur while reading.
42+
*
43+
* @param response A successful HttpResponse.
44+
* @param content Content read from the response.
45+
*/
46+
public IncomingHttpResponse(HttpResponse response, @Nullable String content) {
3947
checkNotNull(response, "response must not be null");
4048
this.statusCode = response.getStatusCode();
4149
this.content = content;
4250
this.headers = ImmutableMap.copyOf(response.getHeaders());
4351
this.request = new OutgoingHttpRequest(response.getRequest());
4452
}
4553

46-
IncomingHttpResponse(HttpResponseException e, HttpRequest request) {
54+
/**
55+
* Creates an IncomingHttpResponse from an HTTP error response.
56+
*
57+
* @param e HttpResponseException representing the HTTP error response.
58+
* @param request The HttpRequest that resulted in the error.
59+
*/
60+
public IncomingHttpResponse(HttpResponseException e, HttpRequest request) {
4761
this(e, new OutgoingHttpRequest(request));
4862
}
4963

src/main/java/com/google/firebase/internal/ApiClientUtils.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,22 @@ public class ApiClientUtils {
4343
* @return A new {@code HttpRequestFactory} instance.
4444
*/
4545
public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) {
46+
return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG);
47+
}
48+
49+
/**
50+
* Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
51+
* automatic retries.
52+
*
53+
* @param app {@link FirebaseApp} from which to obtain authorization credentials.
54+
* @param retryConfig {@link RetryConfig} which specifies how and when to retry errors.
55+
* @return A new {@code HttpRequestFactory} instance.
56+
*/
57+
public static HttpRequestFactory newAuthorizedRequestFactory(
58+
FirebaseApp app, @Nullable RetryConfig retryConfig) {
4659
HttpTransport transport = app.getOptions().getHttpTransport();
4760
return transport.createRequestFactory(
48-
new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG));
61+
new FirebaseRequestInitializer(app, retryConfig));
4962
}
5063

5164
public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.api.client.http.HttpResponseException;
23+
import com.google.api.client.http.HttpStatusCodes;
24+
import com.google.common.base.Strings;
25+
import com.google.common.collect.ImmutableMap;
26+
import com.google.firebase.ErrorCode;
27+
import com.google.firebase.FirebaseException;
28+
import com.google.firebase.IncomingHttpResponse;
29+
import java.util.Map;
30+
31+
/**
32+
* An abstract HttpErrorHandler implementation that maps HTTP status codes to Firebase error codes.
33+
*/
34+
public abstract class BaseHttpErrorHandler<T extends FirebaseException>
35+
implements HttpErrorHandler<T> {
36+
37+
private static final Map<Integer, ErrorCode> HTTP_ERROR_CODES =
38+
ImmutableMap.<Integer, ErrorCode>builder()
39+
.put(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, ErrorCode.INVALID_ARGUMENT)
40+
.put(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, ErrorCode.UNAUTHENTICATED)
41+
.put(HttpStatusCodes.STATUS_CODE_FORBIDDEN, ErrorCode.PERMISSION_DENIED)
42+
.put(HttpStatusCodes.STATUS_CODE_NOT_FOUND, ErrorCode.NOT_FOUND)
43+
.put(HttpStatusCodes.STATUS_CODE_CONFLICT, ErrorCode.CONFLICT)
44+
.put(429, ErrorCode.RESOURCE_EXHAUSTED)
45+
.put(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, ErrorCode.INTERNAL)
46+
.put(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, ErrorCode.UNAVAILABLE)
47+
.build();
48+
49+
@Override
50+
public final T handleHttpResponseException(
51+
HttpResponseException e, IncomingHttpResponse response) {
52+
ErrorParams params = getErrorParams(e, response);
53+
return this.createException(params);
54+
}
55+
56+
protected ErrorParams getErrorParams(HttpResponseException e, IncomingHttpResponse response) {
57+
ErrorCode code = HTTP_ERROR_CODES.get(e.getStatusCode());
58+
if (code == null) {
59+
code = ErrorCode.UNKNOWN;
60+
}
61+
62+
String message = String.format("Unexpected HTTP response with status: %d\n%s",
63+
e.getStatusCode(), e.getContent());
64+
return new ErrorParams(code, message, e, response);
65+
}
66+
67+
protected abstract T createException(ErrorParams params);
68+
69+
public static final class ErrorParams {
70+
71+
private final ErrorCode errorCode;
72+
private final String message;
73+
private final HttpResponseException exception;
74+
private final IncomingHttpResponse response;
75+
76+
public ErrorParams(
77+
ErrorCode errorCode,
78+
String message,
79+
@Nullable HttpResponseException e,
80+
@Nullable IncomingHttpResponse response) {
81+
82+
this.errorCode = checkNotNull(errorCode, "errorCode must not be null");
83+
checkArgument(!Strings.isNullOrEmpty(message), "message must not be null or empty");
84+
this.message = message;
85+
this.exception = e;
86+
this.response = response;
87+
}
88+
89+
public ErrorCode getErrorCode() {
90+
return errorCode;
91+
}
92+
93+
public String getMessage() {
94+
return message;
95+
}
96+
97+
@Nullable
98+
public HttpResponseException getException() {
99+
return exception;
100+
}
101+
102+
@Nullable
103+
public IncomingHttpResponse getResponse() {
104+
return response;
105+
}
106+
}
107+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.api.client.http.HttpRequest;
22+
import com.google.api.client.http.HttpRequestFactory;
23+
import com.google.api.client.http.HttpResponse;
24+
import com.google.api.client.http.HttpResponseException;
25+
import com.google.api.client.json.JsonFactory;
26+
import com.google.api.client.json.JsonObjectParser;
27+
import com.google.api.client.json.JsonParser;
28+
import com.google.common.io.CharStreams;
29+
import com.google.firebase.FirebaseApp;
30+
import com.google.firebase.FirebaseException;
31+
import com.google.firebase.IncomingHttpResponse;
32+
import java.io.IOException;
33+
import java.io.InputStreamReader;
34+
35+
/**
36+
* An HTTP client implementation that handles any errors that may occur during HTTP calls, and
37+
* converts them into an instance of FirebaseException.
38+
*/
39+
public final class ErrorHandlingHttpClient<T extends FirebaseException> {
40+
41+
private final HttpRequestFactory requestFactory;
42+
private final JsonFactory jsonFactory;
43+
private final HttpErrorHandler<T> errorHandler;
44+
45+
public ErrorHandlingHttpClient(
46+
FirebaseApp app, HttpErrorHandler<T> errorHandler, RetryConfig retryConfig) {
47+
this(
48+
ApiClientUtils.newAuthorizedRequestFactory(app, retryConfig),
49+
app.getOptions().getJsonFactory(),
50+
errorHandler);
51+
}
52+
53+
public ErrorHandlingHttpClient(HttpRequestFactory requestFactory,
54+
JsonFactory jsonFactory, HttpErrorHandler<T> errorHandler) {
55+
this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null");
56+
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
57+
this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null");
58+
}
59+
60+
/**
61+
* Sends the given HTTP request to the target endpoint, and parses the response while handling
62+
* any errors that may occur along the way.
63+
*
64+
* @param requestInfo Outgoing request configuration.
65+
* @param responseType Class to parse the response into.
66+
* @param <V> Parsed response type.
67+
* @return Parsed response object.
68+
* @throws T If any error occurs while making the request.
69+
*/
70+
public <V> V sendAndParse(HttpRequestInfo requestInfo, Class<V> responseType) throws T {
71+
IncomingHttpResponse response = send(requestInfo);
72+
return parse(response, responseType);
73+
}
74+
75+
private IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T {
76+
HttpRequest request = createHttpRequest(requestInfo);
77+
78+
HttpResponse response = null;
79+
try {
80+
response = request.execute();
81+
// Read and buffer the content. Otherwise if a parse error occurs later,
82+
// we lose the content stream.
83+
String content = CharStreams.toString(
84+
new InputStreamReader(response.getContent(), response.getContentCharset()));
85+
return new IncomingHttpResponse(response, content);
86+
} catch (HttpResponseException e) {
87+
throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request));
88+
} catch (IOException e) {
89+
throw errorHandler.handleIOException(e);
90+
} finally {
91+
ApiClientUtils.disconnectQuietly(response);
92+
}
93+
}
94+
95+
private <V> V parse(IncomingHttpResponse response, Class<V> responseType) throws T {
96+
try {
97+
JsonParser parser = jsonFactory.createJsonParser(response.getContent());
98+
return parser.parseAndClose(responseType);
99+
} catch (IOException e) {
100+
throw errorHandler.handleParseException(e, response);
101+
}
102+
}
103+
104+
private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T {
105+
try {
106+
return requestInfo.newHttpRequest(requestFactory)
107+
.setParser(new JsonObjectParser(jsonFactory));
108+
} catch (IOException e) {
109+
// Handle request initialization errors (credential loading and other config errors)
110+
throw errorHandler.handleIOException(e);
111+
}
112+
}
113+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import com.google.api.client.http.HttpResponseException;
20+
import com.google.firebase.FirebaseException;
21+
import com.google.firebase.IncomingHttpResponse;
22+
import java.io.IOException;
23+
24+
/**
25+
* An interface for handling all sorts of exceptions that may occur while making an HTTP call and
26+
* converting them into some instance of FirebaseException.
27+
*/
28+
public interface HttpErrorHandler<T extends FirebaseException> {
29+
30+
/**
31+
* Handle any low-level transport and initialization errors.
32+
*/
33+
T handleIOException(IOException e);
34+
35+
/**
36+
* Handle HTTP response exceptions (caused by HTTP error responses).
37+
*/
38+
T handleHttpResponseException(HttpResponseException e, IncomingHttpResponse response);
39+
40+
/**
41+
* Handle any errors that may occur while parsing the response payload.
42+
*/
43+
T handleParseException(IOException e, IncomingHttpResponse response);
44+
}

0 commit comments

Comments
 (0)