Skip to content

Refactor HTTP retries (#3325) #3590

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 31 commits into from
Nov 9, 2022
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb49d16
Break out networking related OS error codes
nehalvpatel Oct 5, 2022
f4dd5a2
Add mapException feature to retry util
nehalvpatel Oct 5, 2022
e86748a
Add WIP sendWithRetries method
nehalvpatel Oct 6, 2022
b985c37
Fix potential runtime null response in onRetry option
nehalvpatel Oct 6, 2022
f7ae46a
Remove code to get CI working
nehalvpatel Oct 6, 2022
d05678d
Apply suggestions from code review
nehalvpatel Oct 7, 2022
4aed216
Update Pub HTTP exceptions
nehalvpatel Oct 8, 2022
d60e6bc
Remove stack trace from retry methods
nehalvpatel Oct 8, 2022
f107cce
Remove hard-coded domain check for HTTP 500
nehalvpatel Oct 8, 2022
f7d1ee5
Simplify method
nehalvpatel Oct 27, 2022
238e97c
Align retry behavior with requirements
nehalvpatel Oct 28, 2022
4d245ad
Move withAuthenticatedClient a layer up and clarify comment
nehalvpatel Oct 28, 2022
1f18d5c
Clarify usage of global HTTP client
nehalvpatel Oct 31, 2022
c11c008
Take request metadata out of _PubHttpClient
nehalvpatel Nov 2, 2022
60edd81
Use RetryClient for OAuth2 calls
nehalvpatel Nov 2, 2022
6d8e7be
Retrigger checks
nehalvpatel Nov 2, 2022
72e47c9
Wrap versions HTTP call with retryForHttp
nehalvpatel Nov 2, 2022
b57b58f
Add more comments
nehalvpatel Nov 2, 2022
3f23c43
Use "throwIfNotOk" in versions HTTP call
nehalvpatel Nov 2, 2022
e288e14
Change PackageIntegrityException to always subclass as an intermitten…
nehalvpatel Nov 4, 2022
d962237
Configure OAuth2 HTTP retries
nehalvpatel Nov 4, 2022
26b12d1
Make metadata headers method an extension on http.Request and use .th…
nehalvpatel Nov 6, 2022
ccc3e30
Add retries to OIDC discovery document request
nehalvpatel Nov 7, 2022
a686043
Add some retries to upload command
nehalvpatel Nov 7, 2022
19f5f01
Add retries to upload package step
nehalvpatel Nov 8, 2022
fde54cc
Fix indentation
nehalvpatel Nov 8, 2022
7afe52c
Make PubHttpResponseException use BaseResponse
nehalvpatel Nov 8, 2022
a38f45a
Fix get_test
nehalvpatel Nov 9, 2022
afe16ee
Fix more tests
nehalvpatel Nov 9, 2022
9ef523f
Fix SHA-256 tests
nehalvpatel Nov 9, 2022
d29d8ee
Only send pub.dev API Accept header when necessary and fix tests
nehalvpatel Nov 9, 2022
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
21 changes: 8 additions & 13 deletions lib/src/authentication/client.dart
Original file line number Diff line number Diff line change
@@ -48,19 +48,14 @@ class _AuthenticatedClient extends http.BaseClient {
await _credential!.getAuthorizationHeaderValue();
}

try {
final response = await _inner.send(request);
if (response.statusCode == 401) {
_detectInvalidCredentials = true;
_throwAuthException(response);
}
return response;
} on PubHttpException catch (e) {
if (e.response.statusCode == 403) {
_throwAuthException(e.response);
}
rethrow;
final response = await _inner.send(request);
if (response.statusCode == 401) {
_detectInvalidCredentials = true;
}
if (response.statusCode == 401 || response.statusCode == 403) {
_throwAuthException(response);
}
return response;
}

/// Throws [AuthenticationException] that includes response status code and
@@ -127,7 +122,7 @@ Future<T> withAuthenticatedClient<T>(
Future<T> Function(http.Client) fn,
) async {
final credential = systemCache.tokenStore.findCredential(hostedUrl);
final client = _AuthenticatedClient(httpClient, credential);
final client = _AuthenticatedClient(globalHttpClient, credential);

try {
return await fn(client);
6 changes: 4 additions & 2 deletions lib/src/command.dart
Original file line number Diff line number Diff line change
@@ -238,7 +238,7 @@ and attaching the relevant parts of that log file.
log.message('Logs written to $transcriptPath.');
}
}
httpClient.close();
globalHttpClient.close();
}
}

@@ -253,7 +253,9 @@ and attaching the relevant parts of that log file.
exception = exception.innerError!;
}

if (exception is HttpException ||
if (exception is PackageIntegrityException) {
return exit_codes.TEMP_FAIL;
} else if (exception is HttpException ||
exception is http.ClientException ||
exception is SocketException ||
exception is TlsException ||
69 changes: 43 additions & 26 deletions lib/src/command/lish.dart
Original file line number Diff line number Diff line change
@@ -93,34 +93,51 @@ class LishCommand extends PubCommand {

try {
await log.progress('Uploading', () async {
var newUri = host.resolve('api/packages/versions/new');
var response = await client.get(newUri, headers: pubApiHeaders);
var parameters = parseJsonResponse(response);
/// 1. Initiate upload
final parametersResponse =
await retryForHttp('initiating upload', () async {
final request =
http.Request('GET', host.resolve('api/packages/versions/new'));
request.attachPubApiHeaders();
request.attachMetadataHeaders();
return await client.fetch(request);
});
final parameters = parseJsonResponse(parametersResponse);

var url = _expectField(parameters, 'url', response);
if (url is! String) invalidServerResponse(response);
/// 2. Upload package
var url = _expectField(parameters, 'url', parametersResponse);
if (url is! String) invalidServerResponse(parametersResponse);
cloudStorageUrl = Uri.parse(url);
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var request = http.MultipartRequest('POST', cloudStorageUrl!);

var fields = _expectField(parameters, 'fields', response);
if (fields is! Map) invalidServerResponse(response);
fields.forEach((key, value) {
if (value is! String) invalidServerResponse(response);
request.fields[key] = value;
final uploadResponse =
await retryForHttp('uploading package', () async {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var request = http.MultipartRequest('POST', cloudStorageUrl!);

var fields = _expectField(parameters, 'fields', parametersResponse);
if (fields is! Map) invalidServerResponse(parametersResponse);
fields.forEach((key, value) {
if (value is! String) invalidServerResponse(parametersResponse);
request.fields[key] = value;
});

request.followRedirects = false;
request.files.add(http.MultipartFile.fromBytes('file', packageBytes,
filename: 'package.tar.gz'));
return await client.fetch(request);
});

request.followRedirects = false;
request.files.add(http.MultipartFile.fromBytes('file', packageBytes,
filename: 'package.tar.gz'));
var postResponse =
await http.Response.fromStream(await client.send(request));

var location = postResponse.headers['location'];
if (location == null) throw PubHttpException(postResponse);
handleJsonSuccess(
await client.get(Uri.parse(location), headers: pubApiHeaders));
/// 3. Finalize publish
var location = uploadResponse.headers['location'];
if (location == null) throw PubHttpResponseException(uploadResponse);
final finalizeResponse =
await retryForHttp('finalizing publish', () async {
final request = http.Request('GET', Uri.parse(location));
request.attachPubApiHeaders();
request.attachMetadataHeaders();
return await client.fetch(request);
});
handleJsonSuccess(finalizeResponse);
});
} on AuthenticationException catch (error) {
var msg = '';
@@ -138,7 +155,7 @@ class LishCommand extends PubCommand {
msg += '\n${error.serverMessage!}\n';
}
dataError(msg + log.red('Authentication failed!'));
} on PubHttpException catch (error) {
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (url == cloudStorageUrl) {
// TODO(nweiz): the response may have XML-formatted information about
@@ -189,7 +206,7 @@ class LishCommand extends PubCommand {
return _publishUsingClient(packageBytes, client);
});
}
} on PubHttpException catch (error) {
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
handleJsonError(error.response);
6 changes: 2 additions & 4 deletions lib/src/command/login.dart
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import 'dart:convert';

import '../command.dart';
import '../command_runner.dart';
import '../http.dart';
import '../log.dart' as log;
import '../oauth2.dart' as oauth2;

@@ -46,9 +45,8 @@ class LoginCommand extends PubCommand {

Future<_UserInfo?> _retrieveUserInfo() async {
return await oauth2.withClient(cache, (client) async {
final discovery = await httpClient.get(Uri.https(
'accounts.google.com', '/.well-known/openid-configuration'));
final userInfoEndpoint = json.decode(discovery.body)['userinfo_endpoint'];
final discovery = await oauth2.fetchOidcDiscoveryDocument();
final userInfoEndpoint = discovery['userinfo_endpoint'];
final userInfoRequest = await client.get(Uri.parse(userInfoEndpoint));
if (userInfoRequest.statusCode != 200) return null;
try {
10 changes: 4 additions & 6 deletions lib/src/exceptions.dart
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import 'package:stack_trace/stack_trace.dart';
import 'package:yaml/yaml.dart';

import 'dart.dart';
import 'http.dart';

/// An exception class for exceptions that are intended to be seen by the user.
///
@@ -106,12 +107,9 @@ class PackageNotFoundException extends WrappedException {
}

/// A class for exceptions where a package's checksum could not be validated.
class PackageIntegrityException extends WrappedException {
PackageIntegrityException(
String message, {
Object? innerError,
StackTrace? innerTrace,
}) : super(message, innerError, innerTrace);
class PackageIntegrityException extends PubHttpException {
PackageIntegrityException(String message)
: super(message, isIntermittent: true);
}

/// Returns whether [error] is a user-facing error object.
Loading