Home > Blockchain >  Setting timeout callback on a HttpClientRequest future throws unhandled exception
Setting timeout callback on a HttpClientRequest future throws unhandled exception

Time:06-11

I'm trying to set the onTimeout callback on a HttpClientRequest future's timeout method.

The example below compiles without error. However, when run it throws the following unhandled exception:

type '() => Future<HttpClientRequest>' is not a subtype of type '(() => FutureOr<_HttpClientRequest>)?' of 'onTimeout'.

I'm new to dart/flutter and am having trouble understanding why the callback method fails.

void main() async {
  requestify();
}

Future<HttpClientRequest> requestify() async {
  var client = HttpClient();
  var request = client.get('10.255.255.1', 80, '');
  // request.timeout(const Duration(seconds: 1));
  request.timeout(const Duration(seconds: 1), onTimeout: () {
    print('timed out');
    return request;
  });
  return request;
}

CodePudding user response:

Let's look at the type error more closely:

type '() => Future<HttpClientRequest>' is not a subtype of type '(() => FutureOr<_HttpClientRequest>)?' of 'onTimeout'.

You supply a callback function that returns a Future<HttpClientRequest>.

What's expected is a function that returns a FutureOr<_HttpClientRequest>.

FutureOr<T> is a special union of types T and Future<T>, so a function that returns a Future<HttpCientRequest> is a subtype of (i.e., is compatible with) a function that returns a FutureOr<HttpClientRequest>. However, what's expected is a FutureOr<_HttpClientRequest>. _HttpClientRequest? Where did that come from, and why isn't that compatible?

Even without knowing the implementation details, we can presume that _HttpClientRequest is some subtype of HttpClientRequest. The error message implies that when we call HttpClient.get, the static (known at compile-time) type of the returned object is Future<HttpClientRequest>, but the actual runtime type is Future<_HttpClientRequest>. If type Derived is a subtype of Base, then Dart also treats GenericClass<Derived> as a subtype of GenericClass<Base>. Normally this is okay, and returning a narrower subtype of what a function is declared to return is safe.

The problem occurs when you then try to call .timeout on that returned Future<_HttpClientRequest>. Future<T>.timeout's callback must return a FutureOr<T>. However, you're calling .timeout on an object whose runtime type is Future<_HttpClientRequest>, so your callback must return an _HttpClientRequest too. Returning the base class type (HttpClientRequest) is not valid since you can't return a broader type where a narrower type is expected.

(See Dart gives Unhandled Exception: type is not a subtype of type of 'value' for another case where treating GenericClass<Derived> as a subtype of GenericClass<Base> can lead to surprising runtime errors.)

TL;DR

As far as the analyzer and compiler know, you're calling .timeout on a Future<HttpClientRequest> with a callback that returns an HttpClientRequest, so there's no compile-time error. However, at runtime, you're actually calling .timeout on a Future<_HttpClientRequest> with a callback that returns an HttpClientRequest, so you end up with a runtime error.

How can you fix this?

Some options:

  • Consider filing a Dart SDK issue about HttpClient.get returning an object whose runtime type is Future<_HttpClientRequest> instead of a Future<HttpClientRequest>.
  • Since _HttpClientRequest is a private type, you can't actually return an object of that type yourself. You can, however, use Future.value to construct a new Future<HttpClientRequest> out of the Future<_HttpClientRequest>:
      // `client.get` might return a `Future` that completes to a subtype of
      // `HttpClientRequest`.
      var request = Future<HttpClientRequest>.value(client.get('10.255.255.1', 80, ''));
      request.timeout(const Duration(seconds: 1), onTimeout: () {
        print('timed out');
        return request;
      });
      return request;
    }
    
  • Avoid the weirdness with the callback type by not using Future.timeout with a callback and instead catching the resulting TimeoutException.

I'll also point out that Future.timeout returns a new Future, but you're returning the original Future, and the value returned by the timeout callback won't ever be used. I don't know if that's what you intend, although in this case it's probably fine if all you want to do is log that a request took too long.

CodePudding user response:

Future<HttpClientRequest?> requestify() async {
  try {
    var client = HttpClient();
    var request = await client.get("10.255.255.1", 80, "")
                       .timeout(const Duration(seconds: 1));
    return request;
  } on TimeoutException catch(e) {
    return null;
  }
}

then in the main function

final response = await requestify();
if (response != null) {
   // HttpClientRequest
} else {
   // timeout
}
  • Related