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

Conversation

nehalvpatel
Copy link
Contributor

@nehalvpatel nehalvpatel commented Oct 6, 2022

UPDATE FROM 10/26/2022 - Plan:

  • http.dart exposes:
    • retryHttpLogic / retryForHttp
      • Accepts a function argument
      • Retries with exponential backoff. Default max = 7
      • Does retry when:
        • HTTP 408, 429
        • HTTP 5xx errors
        • HttpException, SocketException, TlsException, WebSocketException, PubHttpException if PubHttpException.isIntermittent
          • PackageIntegrityException will subclass PubHttpException and be used for call-site specific errors. For example: crc32c validation and sha256 validation
      • If retries fail, throws ApplicationException (investigation of existing behavior needed)
        • 404 by solver (PubHttpException)
        • SocketException?
      • Logs exceptions at a high level (DEBUG)?
      • Logs retry log at a high level (INFO/DEBUG)?
    • extension on http.BaseResponse called throwIfNotOk()
    • globalHttpClient
    • attachRequestMetadata(http.Request request)
      • attaches the x-pub-os / command, etc.. headers from zone variables
      • Stuff that _PubHttpClient currently does
      • Used when making requests to list-versions and download archive
  • in oauth2.dart, wrap http.Client in RetryClient and pass to package:oauth2

The goal is to simplify and condense the retry logic. A future PR will address the other items in #3325.

@nehalvpatel nehalvpatel force-pushed the refactor-http-retries branch 2 times, most recently from a531c2f to ce049fc Compare October 28, 2022 20:56
Copy link
Member

@jonasfj jonasfj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good, I only have nits to suggest.

statusCode == HttpStatus.tooManyRequests) {
// Throw if the response indicates a server error or an intermittent
// client error, but mark it as intermittent so it can be retried.
throw PubHttpResponseException(this as http.Response,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have to cast, maybe it's better to do extension Throwing on http.Response or, make PubHttpResponseException accept an BaseResponse

Copy link
Contributor Author

@nehalvpatel nehalvpatel Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending http.Response breaks the extension for the _download method, which uses http.StreamedResponse. Vice-versa for if we extend http.StreamedResponse, since the versions method uses http.Response.


The change to make PubHttpResponseException accept http.BaseResponse works, but causes build errors in lish.dart:

Screen Shot 2022-11-03 at 5 10 14 PM


If we change handleJsonError to accept http.BaseResponse, we get some more errors:

Screen Shot 2022-11-03 at 5 11 17 PM


And then finally, if we change parseJsonResponse to accept http.BaseResponse:

Screen Shot 2022-11-03 at 5 12 24 PM


So, the cast was the only way I could come up with to make it all work. 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, we can clean it up later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option could be to create PubHttpResponse(BaseResponse, List<int>? body); so that it has an optional body.

But maybe this is better left for a follow up PR...

Copy link
Member

@jonasfj jonasfj Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we actually have: #3130

For refactoring handleJsonError, I think if we're going to make better output -- that's also a great opportunity to re-think the API interface and use something like BaseResponse instead.


Actually, it might be cleaner to cast from BaseResponse to Response in handleJsonError, because -- if it's not a Response then it's same as not being able to read the body. So we could do something like:

void handleJsonError(http.BaseResponse response) {
  if (response is http.Response) { // this is a cast, but also handle cases where the cast fails
    var errorMap = parseJsonResponse(response);
    ... // what we did before
  }
  invalidServerResponse(response);
}

Copy link
Contributor Author

@nehalvpatel nehalvpatel Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast from BaseResponse to Response looks good!

I ended up having to implement that in this PR because the cast in throwIfNotOk was causing tests to fail.

pub/lib/src/http.dart

Lines 192 to 205 in 7afe52c

void handleJsonError(http.BaseResponse response) {
if (response is! http.Response) {
// Not likely to be a common code path, but necessary.
// See https://github.com/dart-lang/pub/pull/3590#discussion_r1012978108
fail(log.red('Invalid server response'));
}
var errorMap = parseJsonResponse(response);
if (errorMap['error'] is! Map ||
!errorMap['error'].containsKey('message') ||
errorMap['error']['message'] is! String) {
invalidServerResponse(response);
}
fail(log.red(errorMap['error']['message']));
}

headers['X-Pub-Environment'] = environment;
}

var type = Zone.current[#_dependencyType];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use this symbol here, I at-least I don't think we can. Since it's a private symbol, it should be distinct from the symbol defined in http.dart:

return runZoned(callback, zoneValues: {#_dependencyType: type});

At-least that's my guess.


May I suggest that we make:

extension AttachHeaders on http.Request {
  void attachMetadataHeaders() {
      ...
  }
}

in http.dart.

We'll have to refactor the places that use Client.get and Client.read into creating Request objects and passing them to Client.send.
But I don't think that matters much.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me wonder if we have any tests covering the meta-data sending?

It would probably require changing isPubDevUrl to something like:

  static bool isPubDevUrl(String url) {
    final origin = Uri.parse(url).origin;

    return origin == pubDevUrl ||
        origin == pubDartlangUrl ||
        (runningFromTest &&
            origin == io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']);
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the headers method to an extension in http.dart. 👍

Re: the metadata test, I'd like to do that test but need some clarification on the desired behavior for the metadata URL-checking logic. I've sent you a message on Chat.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we do tests for metadata sending a follow up PR (it's not tests we currently have). And it's probably better to keep this PR small.

@nehalvpatel nehalvpatel force-pushed the refactor-http-retries branch from 86536fd to 3f23c43 Compare November 4, 2022 00:02
Copy link
Contributor

@sigurdm sigurdm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

We don't have to do the metadata tests in this PR - bet they surely would be nice to have

headers['X-Pub-Environment'] = environment;
}

var type = Zone.current[#_dependencyType];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me wonder if we have any tests covering the meta-data sending?

It would probably require changing isPubDevUrl to something like:

  static bool isPubDevUrl(String url) {
    final origin = Uri.parse(url).origin;

    return origin == pubDevUrl ||
        origin == pubDartlangUrl ||
        (runningFromTest &&
            origin == io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']);
  }

@jonasfj
Copy link
Member

jonasfj commented Nov 7, 2022

@nehalvpatel I think if we fix the tests and retry in publishing, then this is pretty much good to go.

We can revisit the types in PubHttpResponse in a follow up PR, maybe while try to fix #3130. It's probably a lot easier to do a few smaller PRs.

@nehalvpatel nehalvpatel force-pushed the refactor-http-retries branch from 7997462 to afe16ee Compare November 9, 2022 01:59
@nehalvpatel nehalvpatel changed the title WIP: Refactor HTTP retries (#3325) Refactor HTTP retries (#3325) Nov 9, 2022
@nehalvpatel nehalvpatel marked this pull request as ready for review November 9, 2022 05:26
///
/// If false is passed for [throwIfNotOk], the response will not be validated.
/// See [http.BaseResponse.throwIfNotOk] extension for validation details.
Future<http.Response> fetch(http.BaseRequest request,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind, but I have mixed feelings -- mostly because it's easy to call send() by accident instead of using fetch().

or rather it's not very obvious that fetch is intended to throw something in certain cases. But maybe, this is something we can refactor later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do:

class PubHttpClient {
  Future<http.Response> fetch(http.BaseRequest request);
  Future<http.StreamingResponse> fetchStream(http.BaseRequest request);

  // For the new cases where you really need it.
  http.Client rawClient();
}

But maybe, we do this sort of thing later. Now that we're moving package:oauth2 into pub -- we could consider refactoring all of that, and just have a fetch method and fetchStream method. But it's a lot more refactoring, let's cleanup later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants