diff --git a/CHANGELOG.md b/CHANGELOG.md index cea3b25ec..7417de125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [fixed] Enabled automatic retries for FCM API calls failing with + HTTP 500 and 503 errors. # v6.8.0 diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 62e4320e4..7506ff8ee 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -19,6 +19,7 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import java.io.IOException; @@ -28,9 +29,23 @@ */ public class ApiClientUtils { + private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(ImmutableList.of(500, 503)) + .setMaxIntervalMillis(60 * 1000) + .build(); + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { HttpTransport transport = app.getOptions().getHttpTransport(); - return transport.createRequestFactory(new FirebaseRequestInitializer(app)); + return transport.createRequestFactory( + new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG)); } public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java new file mode 100644 index 000000000..78eabaf32 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.GenericUrl; +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.HttpUnsuccessfulResponseHandler; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import java.io.IOException; +import org.junit.After; +import org.junit.Test; + +public class ApiClientUtilsTest { + + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testAuthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertTrue(retryHandler instanceof RetryHandlerDecorator); + RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler() + .getRetryConfig(); + assertEquals(4, retryConfig.getMaxRetries()); + assertEquals(60 * 1000, retryConfig.getMaxIntervalMillis()); + assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503)); + } + + @Test + public void testUnauthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + + assertNull(requestFactory.getInitializer()); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertNull(request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertNull(retryHandler); + } + + @Test + public void testDisconnect() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } + + @Test + public void testDisconnectWithErrorSuppression() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(){ + @Override + public void disconnect() throws IOException { + super.disconnect(); + throw new IOException("test error"); + } + }; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } +}