Skip to content

Commit 9930cbe

Browse files
authored
Explicit HTTP retry wrapper: secrets, SMPT auth, upload singer, domain verifier. (#8978)
1 parent 5f9c706 commit 9930cbe

File tree

6 files changed

+52
-48
lines changed

6 files changed

+52
-48
lines changed

app/lib/package/upload_signer_service.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert';
77

88
import 'package:_pub_shared/data/package_api.dart';
9+
import 'package:_pub_shared/utils/http.dart';
910
import 'package:clock/clock.dart';
1011
import 'package:gcloud/service_scope.dart' as ss;
1112
import 'package:googleapis/iam/v1.dart' as iam;
@@ -105,20 +106,22 @@ abstract class UploadSignerService {
105106
class _IamBasedUploadSigner extends UploadSignerService {
106107
final String projectId;
107108
final String email;
108-
final iam.IamApi iamApi;
109+
final http.Client authClient;
109110

110-
_IamBasedUploadSigner(this.projectId, this.email, http.Client client)
111-
: iamApi = iam.IamApi(client);
111+
_IamBasedUploadSigner(this.projectId, this.email, this.authClient);
112112

113113
@override
114114
Future<SigningResult> sign(List<int> bytes) async {
115115
final request = iam.SignBlobRequest()..bytesToSignAsBytes = bytes;
116116
final name = 'projects/$projectId/serviceAccounts/$email';
117-
final iam.SignBlobResponse response =
118-
// TODO: figure out what new API we should use.
119-
// ignore: deprecated_member_use
120-
await iamApi.projects.serviceAccounts.signBlob(request, name);
121-
return SigningResult(email, response.signatureAsBytes);
117+
return await withRetryHttpClient(client: authClient, (client) async {
118+
final iamApi = iam.IamApi(client);
119+
final response =
120+
// TODO: figure out what new API we should use.
121+
// ignore: deprecated_member_use
122+
await iamApi.projects.serviceAccounts.signBlob(request, name);
123+
return SigningResult(email, response.signatureAsBytes);
124+
});
122125
}
123126
}
124127

app/lib/publisher/domain_verifier.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:_pub_shared/utils/http.dart';
56
import 'package:clock/clock.dart';
67
import 'package:gcloud/service_scope.dart' as ss;
78
import 'package:googleapis/searchconsole/v1.dart' as wmx;
89
import 'package:googleapis_auth/googleapis_auth.dart' as auth;
910
import 'package:http/http.dart' as http;
1011
import 'package:logging/logging.dart';
11-
import 'package:retry/retry.dart' show retry;
1212

1313
import '../shared/exceptions.dart' show AuthorizationException;
1414

@@ -47,8 +47,9 @@ class DomainVerifier {
4747
);
4848
try {
4949
// Request list of sites/domains from the Search Console API.
50-
final sites = await retry(
51-
() => wmx.SearchConsoleApi(client).sites.list(),
50+
final sites = await withRetryHttpClient(
51+
client: client,
52+
(client) => wmx.SearchConsoleApi(client).sites.list(),
5253
maxAttempts: 3,
5354
maxDelay: Duration(milliseconds: 500),
5455
retryIf: (e) => e is! auth.AccessDeniedException,

app/lib/service/email/email_sender.dart

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert' show json;
77
import 'dart:io';
88

9+
import 'package:_pub_shared/utils/http.dart';
910
import 'package:clock/clock.dart';
1011
import 'package:gcloud/service_scope.dart' as ss;
1112
import 'package:googleapis/iamcredentials/v1.dart' as iam_credentials;
@@ -264,12 +265,14 @@ class _GmailSmtpRelay extends EmailSenderBase {
264265
/// [_serviceAccountEmail] configured for _domain-wide delegation_ following:
265266
/// https://developers.google.com/identity/protocols/oauth2/service-account
266267
Future<String> _createAccessToken(String sender) async {
267-
final iam = iam_credentials.IAMCredentialsApi(_authClient);
268268
final iat = clock.now().toUtc().millisecondsSinceEpoch ~/ 1000 - 20;
269269
iam_credentials.SignJwtResponse jwtResponse;
270270
try {
271-
jwtResponse = await retry(
272-
() => iam.projects.serviceAccounts.signJwt(
271+
jwtResponse = await withRetryHttpClient(client: _authClient, (
272+
client,
273+
) async {
274+
final iam = iam_credentials.IAMCredentialsApi(client);
275+
return iam.projects.serviceAccounts.signJwt(
273276
iam_credentials.SignJwtRequest()
274277
..payload = json.encode({
275278
'iss': _serviceAccountEmail,
@@ -280,8 +283,8 @@ class _GmailSmtpRelay extends EmailSenderBase {
280283
'sub': sender,
281284
}),
282285
'projects/-/serviceAccounts/$_serviceAccountEmail',
283-
),
284-
);
286+
);
287+
});
285288
} on Exception catch (e, st) {
286289
_logger.severe(
287290
'Signing JWT for sending email failed, '
@@ -294,29 +297,24 @@ class _GmailSmtpRelay extends EmailSenderBase {
294297
);
295298
}
296299

297-
final client = http.Client();
298-
try {
300+
return await withRetryHttpClient((client) async {
299301
// Send a POST request with:
300302
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
301-
return await retry(() async {
302-
final r = await client.post(
303-
_googleOauth2TokenUrl,
304-
body: {
305-
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
306-
'assertion': jwtResponse.signedJwt,
307-
},
303+
final r = await client.post(
304+
_googleOauth2TokenUrl,
305+
body: {
306+
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
307+
'assertion': jwtResponse.signedJwt,
308+
},
309+
);
310+
if (r.statusCode != 200) {
311+
throw SmtpClientAuthenticationException(
312+
'statusCode=${r.statusCode} from $_googleOauth2TokenUrl '
313+
'while trying exchange JWT for access_token',
308314
);
309-
if (r.statusCode != 200) {
310-
throw SmtpClientAuthenticationException(
311-
'statusCode=${r.statusCode} from $_googleOauth2TokenUrl '
312-
'while trying exchange JWT for access_token',
313-
);
314-
}
315-
return json.decode(r.body)['access_token'] as String;
316-
});
317-
} finally {
318-
client.close();
319-
}
315+
}
316+
return json.decode(r.body)['access_token'] as String;
317+
});
320318
}
321319
}
322320

app/lib/service/secret/backend.dart

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import 'dart:async';
66
import 'dart:convert';
77

8+
import 'package:_pub_shared/utils/http.dart';
89
import 'package:clock/clock.dart';
910
import 'package:gcloud/service_scope.dart' as ss;
1011
import 'package:googleapis/secretmanager/v1.dart' as secretmanager;
1112
import 'package:http/http.dart' as http;
1213
import 'package:logging/logging.dart';
1314
import 'package:pub_dev/shared/configuration.dart';
1415
import 'package:pub_dev/shared/monitoring.dart';
15-
import 'package:retry/retry.dart';
1616

1717
import 'models.dart';
1818

@@ -72,17 +72,17 @@ final class GcpSecretBackend extends SecretBackend {
7272

7373
Future<String?> _lookup(String id) async {
7474
try {
75-
final api = secretmanager.SecretManagerApi(_client);
76-
final secret = await retry(
77-
() async => await api.projects.secrets.versions.access(
75+
return await withRetryHttpClient(client: _client, (client) async {
76+
final api = secretmanager.SecretManagerApi(client);
77+
final secret = await api.projects.secrets.versions.access(
7878
'projects/${activeConfiguration.projectId}/secrets/$id/versions/latest',
79-
),
80-
);
81-
final data = secret.payload?.data;
82-
if (data == null) {
83-
return null;
84-
}
85-
return utf8.decode(base64.decode(data));
79+
);
80+
final data = secret.payload?.data;
81+
if (data == null) {
82+
return null;
83+
}
84+
return utf8.decode(base64.decode(data));
85+
});
8686
} catch (e, st) {
8787
// Log the issue
8888
_log.pubNoticeShout(

app/lib/service/services.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Future<void> withServices(FutureOr<void> Function() fn) async {
106106
)
107107
: loggingEmailSender,
108108
);
109-
registerUploadSigner(await createUploadSigner(retryingAuthClient));
109+
registerUploadSigner(await createUploadSigner(authClient));
110110
registerSecretBackend(GcpSecretBackend(authClient));
111111

112112
// Configure a CloudCompute pool for later use in TaskBackend

pkg/_pub_shared/lib/utils/http.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Future<K> httpGetWithRetry<K>(
8787
Future<K> withRetryHttpClient<K>(
8888
Future<K> Function(http.Client client) fn, {
8989
int maxAttempts = 3,
90+
Duration maxDelay = const Duration(seconds: 30),
9091

9192
/// The HTTP client to use.
9293
///
@@ -109,6 +110,7 @@ Future<K> withRetryHttpClient<K>(
109110
}
110111
},
111112
maxAttempts: maxAttempts,
113+
maxDelay: maxDelay,
112114
retryIf: (e) => _retryIf(e) || (retryIf != null && retryIf(e)),
113115
);
114116
}

0 commit comments

Comments
 (0)