Skip to content

Added ErrorHandlingHttpClient API #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/java/com/google/firebase/FirebaseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public FirebaseException(
*
* @return A Firebase error code.
*/
// TODO: Expose this method publicly
ErrorCode getErrorCode() {
// TODO: Rename this method to getErrorCode when the child classes are refactored.
public ErrorCode getErrorCodeNew() {
return errorCode;
}

Expand Down
18 changes: 16 additions & 2 deletions src/main/java/com/google/firebase/IncomingHttpResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,29 @@ public final class IncomingHttpResponse {
private final Map<String, Object> headers;
private final OutgoingHttpRequest request;

IncomingHttpResponse(HttpResponse response, @Nullable String content) {
/**
* Creates an IncomingHttpResponse from a successful HttpResponse and the content read from it.
* The caller is expected to read the content from the response, and handle any errors that
* may occur while reading.
*
* @param response A successful HttpResponse.
* @param content Content read from the response.
*/
public IncomingHttpResponse(HttpResponse response, @Nullable String content) {
checkNotNull(response, "response must not be null");
this.statusCode = response.getStatusCode();
this.content = content;
this.headers = ImmutableMap.copyOf(response.getHeaders());
this.request = new OutgoingHttpRequest(response.getRequest());
}

IncomingHttpResponse(HttpResponseException e, HttpRequest request) {
/**
* Creates an IncomingHttpResponse from an HTTP error response.
*
* @param e HttpResponseException representing the HTTP error response.
* @param request The HttpRequest that resulted in the error.
*/
public IncomingHttpResponse(HttpResponseException e, HttpRequest request) {
this(e, new OutgoingHttpRequest(request));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.firebase.ErrorCode;
import com.google.firebase.FirebaseException;
import com.google.firebase.IncomingHttpResponse;
import java.util.Map;

/**
* An abstract HttpErrorHandler implementation that maps HTTP status codes to Firebase error codes.
*/
public abstract class AbstractHttpErrorHandler<T extends FirebaseException>
implements HttpErrorHandler<T> {

private static final Map<Integer, ErrorCode> HTTP_ERROR_CODES =
ImmutableMap.<Integer, ErrorCode>builder()
.put(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, ErrorCode.INVALID_ARGUMENT)
.put(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, ErrorCode.UNAUTHENTICATED)
.put(HttpStatusCodes.STATUS_CODE_FORBIDDEN, ErrorCode.PERMISSION_DENIED)
.put(HttpStatusCodes.STATUS_CODE_NOT_FOUND, ErrorCode.NOT_FOUND)
.put(HttpStatusCodes.STATUS_CODE_CONFLICT, ErrorCode.CONFLICT)
.put(429, ErrorCode.RESOURCE_EXHAUSTED)
.put(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, ErrorCode.INTERNAL)
.put(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, ErrorCode.UNAVAILABLE)
.build();

@Override
public final T handleHttpResponseException(
HttpResponseException e, IncomingHttpResponse response) {
ErrorParams params = getErrorParams(e, response);
return this.createException(params);
}

protected ErrorParams getErrorParams(HttpResponseException e, IncomingHttpResponse response) {
ErrorCode code = HTTP_ERROR_CODES.get(e.getStatusCode());
if (code == null) {
code = ErrorCode.UNKNOWN;
}

String message = String.format("Unexpected HTTP response with status: %d\n%s",
e.getStatusCode(), e.getContent());
return new ErrorParams(code, message, e, response);
}

protected abstract T createException(ErrorParams params);

public static final class ErrorParams {

private final ErrorCode errorCode;
private final String message;
private final HttpResponseException exception;
private final IncomingHttpResponse response;

public ErrorParams(
ErrorCode errorCode,
String message,
@Nullable HttpResponseException e,
@Nullable IncomingHttpResponse response) {

this.errorCode = checkNotNull(errorCode, "errorCode must not be null");
checkArgument(!Strings.isNullOrEmpty(message), "message must not be null or empty");
this.message = message;
this.exception = e;
this.response = response;
}

public ErrorCode getErrorCode() {
return errorCode;
}

public String getMessage() {
return message;
}

@Nullable
public HttpResponseException getException() {
return exception;
}

@Nullable
public IncomingHttpResponse getResponse() {
return response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.internal;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.http.HttpResponseException;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.Key;
import com.google.common.base.Strings;
import com.google.firebase.ErrorCode;
import com.google.firebase.FirebaseException;
import com.google.firebase.IncomingHttpResponse;
import java.io.IOException;

/**
* An abstract HttpErrorHandler that handles Google Cloud error responses. Format of these
* error responses are defined at https://cloud.google.com/apis/design/errors.
*/
public abstract class AbstractPlatformErrorHandler<T extends FirebaseException>
extends AbstractHttpErrorHandler<T> {

protected final JsonFactory jsonFactory;

public AbstractPlatformErrorHandler(JsonFactory jsonFactory) {
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
}

@Override
protected final ErrorParams getErrorParams(
HttpResponseException e, IncomingHttpResponse response) {
ErrorParams defaults = super.getErrorParams(e, response);
PlatformErrorResponse parsedError = this.parseErrorResponse(e.getContent());

ErrorCode code = defaults.getErrorCode();
String status = parsedError.getStatus();
if (!Strings.isNullOrEmpty(status)) {
code = Enum.valueOf(ErrorCode.class, parsedError.getStatus());
}

String message = parsedError.getMessage();
if (Strings.isNullOrEmpty(message)) {
message = defaults.getMessage();
}

return new ErrorParams(code, message, e, response);
}

private PlatformErrorResponse parseErrorResponse(String content) {
PlatformErrorResponse response = new PlatformErrorResponse();
if (!Strings.isNullOrEmpty(content)) {
try {
jsonFactory.createJsonParser(content).parseAndClose(response);
} catch (IOException e) {
// Ignore any error that may occur while parsing the error response. The server
// may have responded with a non-json payload. Return an empty return value, and
// let the base class logic come into play.
}
}

return response;
}

public static class PlatformErrorResponse {
@Key("error")
private PlatformError error;

String getStatus() {
return error != null ? error.status : null;
}

String getMessage() {
return error != null ? error.message : null;
}
}

public static class PlatformError {
@Key("status")
private String status;

@Key("message")
private String message;
}
}
15 changes: 14 additions & 1 deletion src/main/java/com/google/firebase/internal/ApiClientUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,22 @@ public class ApiClientUtils {
* @return A new {@code HttpRequestFactory} instance.
*/
public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) {
return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG);
}

/**
* Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
* automatic retries.
*
* @param app {@link FirebaseApp} from which to obtain authorization credentials.
* @param retryConfig {@link RetryConfig} which specifies how and when to retry errors.
* @return A new {@code HttpRequestFactory} instance.
*/
public static HttpRequestFactory newAuthorizedRequestFactory(
FirebaseApp app, @Nullable RetryConfig retryConfig) {
HttpTransport transport = app.getOptions().getHttpTransport();
return transport.createRequestFactory(
new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG));
new FirebaseRequestInitializer(app, retryConfig));
}

public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.internal;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.JsonParser;
import com.google.common.io.CharStreams;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseException;
import com.google.firebase.IncomingHttpResponse;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* An HTTP client implementation that handles any errors that may occur during HTTP calls, and
* converts them into an instance of FirebaseException.
*/
public final class ErrorHandlingHttpClient<T extends FirebaseException> {

private final HttpRequestFactory requestFactory;
private final JsonFactory jsonFactory;
private final HttpErrorHandler<T> errorHandler;

public ErrorHandlingHttpClient(
FirebaseApp app, HttpErrorHandler<T> errorHandler, RetryConfig retryConfig) {
this(
ApiClientUtils.newAuthorizedRequestFactory(app, retryConfig),
app.getOptions().getJsonFactory(),
errorHandler);
}

public ErrorHandlingHttpClient(
HttpRequestFactory requestFactory,
JsonFactory jsonFactory,
HttpErrorHandler<T> errorHandler) {
this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null");
this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null");
this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null");
}

/**
* Sends the given HTTP request to the target endpoint, and parses the response while handling
* any errors that may occur along the way.
*
* @param requestInfo Outgoing request configuration.
* @param responseType Class to parse the response into.
* @param <V> Parsed response type.
* @return Parsed response object.
* @throws T If any error occurs while making the request.
*/
public <V> V sendAndParse(HttpRequestInfo requestInfo, Class<V> responseType) throws T {
IncomingHttpResponse response = send(requestInfo);
return parse(response, responseType);
}

private IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T {
HttpRequest request = createHttpRequest(requestInfo);

HttpResponse response = null;
try {
response = request.execute();
// Read and buffer the content. Otherwise if a parse error occurs later,
// we lose the content stream.
String content = CharStreams.toString(
new InputStreamReader(response.getContent(), response.getContentCharset()));
return new IncomingHttpResponse(response, content);
} catch (HttpResponseException e) {
throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request));
} catch (IOException e) {
throw errorHandler.handleIOException(e);
} finally {
ApiClientUtils.disconnectQuietly(response);
}
}

private <V> V parse(IncomingHttpResponse response, Class<V> responseType) throws T {
try {
JsonParser parser = jsonFactory.createJsonParser(response.getContent());
return parser.parseAndClose(responseType);
} catch (IOException e) {
throw errorHandler.handleParseException(e, response);
}
}

private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T {
try {
return requestInfo.newHttpRequest(requestFactory)
.setParser(new JsonObjectParser(jsonFactory));
} catch (IOException e) {
// Handle request initialization errors (credential loading and other config errors)
throw errorHandler.handleIOException(e);
}
}
}
Loading