Skip to content

Commit 5e3fd19

Browse files
authored
Added ErrorHandlingHttpClient API (#353)
* Added core error handling abstractions * Added unit tests for the new functionality * Exposed getErrorCode as getErrorCodeNew * Enabled error code assertions
1 parent b269191 commit 5e3fd19

11 files changed

+1011
-9
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public FirebaseException(
5757
*
5858
* @return A Firebase error code.
5959
*/
60-
// TODO: Expose this method publicly
61-
ErrorCode getErrorCode() {
60+
// TODO: Rename this method to getErrorCode when the child classes are refactored.
61+
public ErrorCode getErrorCodeNew() {
6262
return errorCode;
6363
}
6464

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

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 AbstractHttpErrorHandler<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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.HttpResponseException;
22+
import com.google.api.client.json.JsonFactory;
23+
import com.google.api.client.util.Key;
24+
import com.google.common.base.Strings;
25+
import com.google.firebase.ErrorCode;
26+
import com.google.firebase.FirebaseException;
27+
import com.google.firebase.IncomingHttpResponse;
28+
import java.io.IOException;
29+
30+
/**
31+
* An abstract HttpErrorHandler that handles Google Cloud error responses. Format of these
32+
* error responses are defined at https://cloud.google.com/apis/design/errors.
33+
*/
34+
public abstract class AbstractPlatformErrorHandler<T extends FirebaseException>
35+
extends AbstractHttpErrorHandler<T> {
36+
37+
protected final JsonFactory jsonFactory;
38+
39+
public AbstractPlatformErrorHandler(JsonFactory jsonFactory) {
40+
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
41+
}
42+
43+
@Override
44+
protected final ErrorParams getErrorParams(
45+
HttpResponseException e, IncomingHttpResponse response) {
46+
ErrorParams defaults = super.getErrorParams(e, response);
47+
PlatformErrorResponse parsedError = this.parseErrorResponse(e.getContent());
48+
49+
ErrorCode code = defaults.getErrorCode();
50+
String status = parsedError.getStatus();
51+
if (!Strings.isNullOrEmpty(status)) {
52+
code = Enum.valueOf(ErrorCode.class, parsedError.getStatus());
53+
}
54+
55+
String message = parsedError.getMessage();
56+
if (Strings.isNullOrEmpty(message)) {
57+
message = defaults.getMessage();
58+
}
59+
60+
return new ErrorParams(code, message, e, response);
61+
}
62+
63+
private PlatformErrorResponse parseErrorResponse(String content) {
64+
PlatformErrorResponse response = new PlatformErrorResponse();
65+
if (!Strings.isNullOrEmpty(content)) {
66+
try {
67+
jsonFactory.createJsonParser(content).parseAndClose(response);
68+
} catch (IOException e) {
69+
// Ignore any error that may occur while parsing the error response. The server
70+
// may have responded with a non-json payload. Return an empty return value, and
71+
// let the base class logic come into play.
72+
}
73+
}
74+
75+
return response;
76+
}
77+
78+
public static class PlatformErrorResponse {
79+
@Key("error")
80+
private PlatformError error;
81+
82+
String getStatus() {
83+
return error != null ? error.status : null;
84+
}
85+
86+
String getMessage() {
87+
return error != null ? error.message : null;
88+
}
89+
}
90+
91+
public static class PlatformError {
92+
@Key("status")
93+
private String status;
94+
95+
@Key("message")
96+
private String message;
97+
}
98+
}

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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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(
54+
HttpRequestFactory requestFactory,
55+
JsonFactory jsonFactory,
56+
HttpErrorHandler<T> errorHandler) {
57+
this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null");
58+
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
59+
this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null");
60+
}
61+
62+
/**
63+
* Sends the given HTTP request to the target endpoint, and parses the response while handling
64+
* any errors that may occur along the way.
65+
*
66+
* @param requestInfo Outgoing request configuration.
67+
* @param responseType Class to parse the response into.
68+
* @param <V> Parsed response type.
69+
* @return Parsed response object.
70+
* @throws T If any error occurs while making the request.
71+
*/
72+
public <V> V sendAndParse(HttpRequestInfo requestInfo, Class<V> responseType) throws T {
73+
IncomingHttpResponse response = send(requestInfo);
74+
return parse(response, responseType);
75+
}
76+
77+
private IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T {
78+
HttpRequest request = createHttpRequest(requestInfo);
79+
80+
HttpResponse response = null;
81+
try {
82+
response = request.execute();
83+
// Read and buffer the content. Otherwise if a parse error occurs later,
84+
// we lose the content stream.
85+
String content = CharStreams.toString(
86+
new InputStreamReader(response.getContent(), response.getContentCharset()));
87+
return new IncomingHttpResponse(response, content);
88+
} catch (HttpResponseException e) {
89+
throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request));
90+
} catch (IOException e) {
91+
throw errorHandler.handleIOException(e);
92+
} finally {
93+
ApiClientUtils.disconnectQuietly(response);
94+
}
95+
}
96+
97+
private <V> V parse(IncomingHttpResponse response, Class<V> responseType) throws T {
98+
try {
99+
JsonParser parser = jsonFactory.createJsonParser(response.getContent());
100+
return parser.parseAndClose(responseType);
101+
} catch (IOException e) {
102+
throw errorHandler.handleParseException(e, response);
103+
}
104+
}
105+
106+
private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T {
107+
try {
108+
return requestInfo.newHttpRequest(requestFactory)
109+
.setParser(new JsonObjectParser(jsonFactory));
110+
} catch (IOException e) {
111+
// Handle request initialization errors (credential loading and other config errors)
112+
throw errorHandler.handleIOException(e);
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)