Skip to content

Commit ef964ec

Browse files
authored
Handle API disabled errors in firebase-appdistribution (#3917)
* Handle API disabled errors in firebase-appdistribution * Fix formatting * Update api.txt
1 parent 8bd97a4 commit ef964ec

File tree

10 files changed

+314
-8
lines changed

10 files changed

+314
-8
lines changed

firebase-appdistribution-api/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ package com.google.firebase.appdistribution {
3030
}
3131

3232
public enum FirebaseAppDistributionException.Status {
33+
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status API_DISABLED;
3334
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status AUTHENTICATION_CANCELED;
3435
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status AUTHENTICATION_FAILURE;
3536
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status DOWNLOAD_FAILURE;

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistributionException.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ public enum Status {
7979
* com.google.firebase:firebase-appdistribution}.
8080
*/
8181
NOT_IMPLEMENTED,
82+
83+
/**
84+
* The Firebase App Distribution Tester API is disabled for this project.
85+
*
86+
* <p>The developer of this app must enable the API in the Google Cloud Console before using the
87+
* App Distribution SDK. See the <a
88+
* href="https://firebase.google.com/docs/app-distribution/set-up-alerts?platform=android">documentation</a>
89+
* for more information. If you enabled this API recently, wait a few minutes for the action to
90+
* propagate to our systems and retry.
91+
*/
92+
API_DISABLED,
8293
}
8394

8495
@NonNull private final Status status;

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ErrorMessages.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.appdistribution.impl;
1616

1717
class ErrorMessages {
18+
1819
static final String NETWORK_ERROR = "Request failed with unknown network error.";
1920

2021
static final String JSON_PARSING_ERROR =
@@ -51,5 +52,8 @@ class ErrorMessages {
5152
static final String APK_INSTALLATION_FAILED =
5253
"The APK failed to install or installation was canceled by the tester.";
5354

55+
static final String API_DISABLED =
56+
"The App Distribution Tester API is disabled. It must be enabled in the Google Cloud Console. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.";
57+
5458
private ErrorMessages() {}
5559
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appdistribution.impl;
16+
17+
import androidx.annotation.Nullable;
18+
import com.google.auto.value.AutoValue;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import org.json.JSONArray;
22+
import org.json.JSONException;
23+
import org.json.JSONObject;
24+
25+
/**
26+
* Details about a {@code SERVICE_DISABLED} error returned by the Tester API.
27+
*
28+
* <p>Error structure is described in the <a
29+
* href="https://cloud.google.com/apis/design/errors#error_details">Cloud APIs documentation</a>.
30+
*/
31+
@AutoValue
32+
abstract class TesterApiDisabledErrorDetails {
33+
34+
@AutoValue
35+
abstract static class HelpLink {
36+
abstract String description();
37+
38+
abstract String url();
39+
40+
static HelpLink create(String description, String url) {
41+
return new AutoValue_TesterApiDisabledErrorDetails_HelpLink(description, url);
42+
}
43+
}
44+
45+
abstract List<HelpLink> helpLinks();
46+
47+
String formatLinks() {
48+
StringBuilder stringBuilder = new StringBuilder();
49+
for (HelpLink link : helpLinks()) {
50+
stringBuilder.append(String.format("%s: %s\n", link.description(), link.url()));
51+
}
52+
return stringBuilder.toString();
53+
}
54+
55+
/**
56+
* Try to parse API disabled error details from a response body.
57+
*
58+
* <p>If the response is an API disabled error but there is a failure parsing the help links, it
59+
* will still return the details with any links it could parse before the failure.
60+
*
61+
* @param responseBody
62+
* @return the details, or {@code null} if the response was not in the expected format
63+
*/
64+
@Nullable
65+
static TesterApiDisabledErrorDetails tryParse(String responseBody) {
66+
try {
67+
// Get the error details object
68+
JSONArray details =
69+
new JSONObject(responseBody).getJSONObject("error").getJSONArray("details");
70+
JSONObject errorInfo = getDetailWithType(details, "type.googleapis.com/google.rpc.ErrorInfo");
71+
if (errorInfo.getString("reason").equals("SERVICE_DISABLED")) {
72+
return new AutoValue_TesterApiDisabledErrorDetails(parseHelpLinks(details));
73+
}
74+
} catch (JSONException e) {
75+
// Error was not in expected API disabled error format
76+
}
77+
return null;
78+
}
79+
80+
private static JSONObject getDetailWithType(JSONArray details, String type) throws JSONException {
81+
for (int i = 0; i < details.length(); i++) {
82+
JSONObject detail = details.getJSONObject(i);
83+
if (detail.getString("@type").equals(type)) {
84+
return detail;
85+
}
86+
}
87+
throw new JSONException("No detail present with type: " + type);
88+
}
89+
90+
static List<HelpLink> parseHelpLinks(JSONArray details) {
91+
List<HelpLink> helpLinks = new ArrayList<>();
92+
try {
93+
JSONObject help = getDetailWithType(details, "type.googleapis.com/google.rpc.Help");
94+
JSONArray linksJson = help.getJSONArray("links");
95+
for (int i = 0; i < linksJson.length(); i++) {
96+
helpLinks.add(parseHelpLink(linksJson.getJSONObject(i)));
97+
}
98+
} catch (JSONException e) {
99+
// If we have an issue parsing the links, we don't want to fail the entire error parsing, so
100+
// go ahead and return what we have
101+
}
102+
return helpLinks;
103+
}
104+
105+
private static HelpLink parseHelpLink(JSONObject json) throws JSONException {
106+
String description = json.getString("description");
107+
String url = json.getString("url");
108+
return HelpLink.create(description, url);
109+
}
110+
}

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterApiHttpClient.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ private static JSONObject readResponse(String tag, HttpsURLConnection connection
182182
String responseBody = readResponseBody(connection);
183183
LogWrapper.getInstance().v(tag, String.format("Response (%d): %s", responseCode, responseBody));
184184
if (!isResponseSuccess(responseCode)) {
185-
throw getExceptionForHttpResponse(tag, responseCode);
185+
throw getExceptionForHttpResponse(tag, responseCode, responseBody);
186186
}
187187
return parseJson(tag, responseBody);
188188
}
@@ -234,18 +234,15 @@ private HttpsURLConnection openHttpsUrlConnection(String url, String authToken)
234234
}
235235

236236
private static FirebaseAppDistributionException getExceptionForHttpResponse(
237-
String tag, int responseCode) {
237+
String tag, int responseCode, String responseBody) {
238238
switch (responseCode) {
239239
case 400:
240240
return getException(tag, "Bad request", Status.UNKNOWN);
241241
case 401:
242242
return getException(tag, ErrorMessages.AUTHENTICATION_ERROR, Status.AUTHENTICATION_FAILURE);
243243
case 403:
244-
return getException(tag, ErrorMessages.AUTHORIZATION_ERROR, Status.AUTHENTICATION_FAILURE);
244+
return getExceptionFor403(tag, responseBody);
245245
case 404:
246-
// TODO(lkellogg): Change this to a different status once 404s no longer indicate missing
247-
// access (the backend should return 403s for those cases, including when the resource
248-
// doesn't exist but the tester doesn't have the access to see that information)
249246
return getException(tag, ErrorMessages.NOT_FOUND_ERROR, Status.AUTHENTICATION_FAILURE);
250247
case 408:
251248
case 504:
@@ -255,6 +252,22 @@ private static FirebaseAppDistributionException getExceptionForHttpResponse(
255252
}
256253
}
257254

255+
private static FirebaseAppDistributionException getExceptionFor403(
256+
String tag, String responseBody) {
257+
// Check if this is an API disabled error
258+
TesterApiDisabledErrorDetails apiDisabledErrorDetails =
259+
TesterApiDisabledErrorDetails.tryParse(responseBody);
260+
if (apiDisabledErrorDetails != null) {
261+
String messageWithHelpLinks =
262+
String.format(
263+
"%s\n\n%s", ErrorMessages.API_DISABLED, apiDisabledErrorDetails.formatLinks());
264+
return getException(tag, messageWithHelpLinks, Status.API_DISABLED);
265+
}
266+
267+
// Otherwise return a basic 403 exception
268+
return getException(tag, ErrorMessages.AUTHORIZATION_ERROR, Status.AUTHENTICATION_FAILURE);
269+
}
270+
258271
private static FirebaseAppDistributionException getException(
259272
String tag, String message, Status status) {
260273
return new FirebaseAppDistributionException(tagMessage(tag, message), status);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"error": {
3+
"code": 403,
4+
"message": "Firebase App Testers API has not been used in project 123456789 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseapptesters.googleapis.com/overview?project=123456789 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
5+
"status": "PERMISSION_DENIED",
6+
"details": [
7+
{
8+
"@type": "type.googleapis.com/google.rpc.Help",
9+
"links": [
10+
{
11+
"description": "One link",
12+
"url": "http://google.com"
13+
},
14+
{
15+
"description": "Another link",
16+
"url": "http://gmail.com"
17+
},
18+
{
19+
"bad": "link"
20+
},
21+
{
22+
"description": "One more link",
23+
"url": "http://somethingelse.com"
24+
}
25+
]
26+
},
27+
{
28+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
29+
"reason": "SERVICE_DISABLED",
30+
"domain": "googleapis.com",
31+
"metadata": {
32+
"consumer": "projects/123456789",
33+
"service": "firebaseapptesters.googleapis.com"
34+
}
35+
}
36+
]
37+
}
38+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"error": {
3+
"code": 403,
4+
"message": "Firebase App Testers API has not been used in project 123456789 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseapptesters.googleapis.com/overview?project=123456789 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
5+
"status": "PERMISSION_DENIED",
6+
"details": [
7+
{
8+
"@type": "type.googleapis.com/google.rpc.Help",
9+
"links": [
10+
{
11+
"description": "Google developers console API activation",
12+
"url": "https://console.developers.google.com/apis/api/firebaseapptesters.googleapis.com/overview?project=123456789"
13+
}
14+
]
15+
},
16+
{
17+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
18+
"reason": "SERVICE_DISABLED",
19+
"domain": "googleapis.com",
20+
"metadata": {
21+
"consumer": "projects/123456789",
22+
"service": "firebaseapptesters.googleapis.com"
23+
}
24+
}
25+
]
26+
}
27+
}

firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/TestUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,12 @@ private static <T> Answer<Task<T>> applyToForegroundActivityTaskAnswer(Activity
138138
};
139139
}
140140

141+
static InputStream getTestFileInputStream(String fileName) throws IOException {
142+
return getContext().getResources().getAssets().open(fileName);
143+
}
144+
141145
static String readTestFile(String fileName) throws IOException {
142-
final InputStream jsonInputStream = getContext().getResources().getAssets().open(fileName);
146+
final InputStream jsonInputStream = getTestFileInputStream(fileName);
143147
return streamToString(jsonInputStream);
144148
}
145149

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appdistribution.impl;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
19+
import com.google.firebase.appdistribution.impl.TesterApiDisabledErrorDetails.HelpLink;
20+
import java.io.IOException;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.robolectric.RobolectricTestRunner;
26+
27+
@RunWith(RobolectricTestRunner.class)
28+
public class TesterApiDisabledErrorDetailsTest {
29+
30+
@Test
31+
public void tryParse_success() throws IOException {
32+
String responseBody = TestUtils.readTestFile("apiDisabledResponse.json");
33+
34+
TesterApiDisabledErrorDetails details = TesterApiDisabledErrorDetails.tryParse(responseBody);
35+
36+
assertThat(details.helpLinks())
37+
.containsExactly(
38+
HelpLink.create(
39+
"Google developers console API activation",
40+
"https://console.developers.google.com/apis/api/firebaseapptesters.googleapis.com/overview?project=123456789"));
41+
}
42+
43+
@Test
44+
public void tryParse_badResponseBody_returnsNull() {
45+
String responseBody = "not json";
46+
47+
TesterApiDisabledErrorDetails details = TesterApiDisabledErrorDetails.tryParse(responseBody);
48+
49+
assertThat(details).isNull();
50+
}
51+
52+
@Test
53+
public void tryParse_errorParsingLinks_stillReturnsDetails() throws IOException {
54+
String responseBody = TestUtils.readTestFile("apiDisabledBadLinkResponse.json");
55+
56+
TesterApiDisabledErrorDetails details = TesterApiDisabledErrorDetails.tryParse(responseBody);
57+
58+
assertThat(details.helpLinks())
59+
.containsExactly(
60+
HelpLink.create("One link", "http://google.com"),
61+
HelpLink.create("Another link", "http://gmail.com"));
62+
}
63+
64+
@Test
65+
public void formatLinks_success() {
66+
List<HelpLink> helpLinks = new ArrayList<>();
67+
helpLinks.add(HelpLink.create("One link", "http://google.com"));
68+
helpLinks.add(HelpLink.create("Another link", "http://gmail.com"));
69+
TesterApiDisabledErrorDetails details = new AutoValue_TesterApiDisabledErrorDetails(helpLinks);
70+
71+
String formattedLinks = details.formatLinks();
72+
73+
assertThat(formattedLinks)
74+
.isEqualTo("One link: http://google.com\nAnother link: http://gmail.com\n");
75+
}
76+
}

0 commit comments

Comments
 (0)