Sdk: dart:io HttpClient.post throws uncatch-able exception

Created on 3 Nov 2017  路  10Comments  路  Source: dart-lang/sdk

I have this issue where I use dart:io's HttpClient to post data client.post and that the post throws the following exception:

      if (_nextResponseCompleter == null) {
        throw new HttpException(
            "Unexpected response (unsolicited response without request).",
            uri: _currentUri);
      }

(from http_impl.dart)

when I try to put the client.post in a try-catch block, the exception doesn't bubble up in my catch. Instead, it goes up until it's uncaught and make my program crash.

with the help of @floitschG I could successfully catch the error by issuing my client.post in a runZoned, but this shouldn't be necessary.

This bug affected our system in production and made our server crash.

P2 area-library library-io type-bug

All 10 comments

@zanderso : I looked a little bit into this.
The problem is that this exception is thrown from within the listen to a _httpParser. This means that the user can't ever catch this error:

    _subscription = _httpParser.listen((incoming) {
      // Only handle one incoming response at the time. Keep the
      // stream paused until the response have been processed.
      _subscription.pause();
      // We assume the response is not here, until we have send the request.
      if (_nextResponseCompleter == null) {
        throw new HttpException(
            "Unexpected response (unsolicited response without request).",
            uri: _currentUri);
      }

The big question is: how should this error be reported?. Just throwing isn't good enough, since it would never reach the user. The surrounding code doesn't give me enough information.

When can _nextResponseCompleter == null be true?
If it is null did we already report an error (or success), and should we then just ignore the error? (still better than crashing the VM).

@Pacane could you provide some feedback on how this was happening?
What client/server interaction seems to trigger this?

So this happens when I do a post request to a remote server, and the server gives a 408 response (Request Timeout).

When the server returns the 408, _nextResponseCompleter == null is true, and the exception is thrown.

The production had this pattern

connect() async {
     var client = getSomeClient();
     try {
          HttpClientRequest request = await client.postUrl(connUri);
          // more code
     catch(e) {
          retryConnectWithSomeDelay();
     }
}

For some reason, when I do this

connect() async {
     var client = getSomeClient();
     try {
          HttpClientRequest request = await client.postUrl(connUri);
          // more code
     catch(e) {
          retryConnectWithSomeDelay();
     }
}

the error happens. But if I close the client before retrying:

connect() async {
     var client = getSomeClient();
     try {
          HttpClientRequest request = await client.postUrl(connUri);
          // more code
     catch(e) {
          __client.close();__
          retryConnectWithSomeDelay();
     }
}

I have no error.

Note that I can freely remove the retryConnectWithSomeDelay() and get essentially the same behaviour. In other words, the minimal code needed to reproduce this (omitting some details like certificates and [I think]聽other irrelevant stuff) is:

connect() async {
     var client = getSomeClient();
     HttpClientRequest request = await client.postUrl(connUri);
}

with a server returning a 408. When I debugged the _HttpClientConnection, when the exception was gonna be thrown, the incoming value from the _httpParser.listen() was:

image

Note that this server returning the 408 is a 3rd party server that I don't have access to unfortunately.

I get this when using the Dart AWS Lambda Runtime in the lambci/lambda:provided Docker image in the AWS SAM CLI tool:

Unhandled exception:
HttpException: Unexpected response (unsolicited response without request).

The server interacting with the Dart HTTP client is a Go server.
Specifically the postInvocationError request that is handled by the handleErrorRequest function gives the "Unexpected response" exception.

@ZichangG @sortie

      if (incoming.statusCode == 100) {
        // some codes
      } else {
        _nextResponseCompleter.complete(incoming);
        _nextResponseCompleter = null;
      }
          if (!closing &&
              !_dispose &&
              incoming.headers.persistentConnection &&
              request.persistentConnection) {
            // Return connection, now we are done.
            _httpClient._returnConnection(this);
            _subscription.resume();
          } else {
            destroy();
          }

I'm not confident whether this is a bug. In our implementation, if statusCode != 100, we complete with incoming and set _nextResponseCompleter to null. Because of persistent connection, it resumes the subscription so the next incoming arrived and _nextResponseCompleter is null.
Is this behavior correct?

I'm wondering whether it's a bug in the failing request or the request that came before.
Is there a possibility that the previous request is causing the unexpected response?
How is this handled in other languages?

According to RFC,

A server SHOULD send the "close" connection option
(Section 6.1 of [RFC7230]) in the response, since 408 implies that
the server has decided to close the connection rather than continue
waiting.

This looks like the case where server didn't explicitly close.

@Schwusch or @Pacane Could you please try my patch(https://dart-review.googlesource.com/c/sdk/+/144346) to see whether it worked?

@zichangg I'll make an attempt to download the patchset and build the SDK today, or is there a build artifact or alike from the CI/CD?

@zichangg It worked when I tested it! You're awesome :1st_place_medal:

@Schwusch Thanks! I'll try to get it landed asap.

Was this page helpful?
0 / 5 - 0 ratings