feat(http2): add a pooled, multiplexed HTTP/2 http.Client - #1956
feat(http2): add a pooled, multiplexed HTTP/2 http.Client#1956demolaf wants to merge 27 commits into
Conversation
brianquinlan
left a comment
There was a problem hiding this comment.
I'll have more review feedback tomorrow. Also, this PR adds (preliminary) conformance tests: #1960
You might want to add them to this PR.
| /// networks - do not use it to accept arbitrary certificates in production. | ||
| class Http2Client extends BaseClient { | ||
| Http2Client({ | ||
| this.maxStreamsPerConnection = 100, |
There was a problem hiding this comment.
Should this default to SETTINGS_MAX_CONCURRENT_STREAMS send by the server?
There was a problem hiding this comment.
yes, this should read SETTINGS_MAX_CONCURRENT_STREAMS returned from the server. i'll make that change now
There was a problem hiding this comment.
_settingsHandler.peerSettings.maxConcurrentStreams isn't exposed in the public API as of now. So I'll need to add a getter e.g. peerMaxConcurrentStreams on ClientTransportConnection which can then be used in Http2Client.
There was a problem hiding this comment.
Kept the getter on ClientConnection rather than ClientTransportConnection
…xConcurrentStreams
e8b8590 to
39b75f9
Compare
|
@brianquinlan @mosuem I'm deferring the TODOs in the conformance tests to a later PR since this is already quite a lot, sounds good? |
| @@ -0,0 +1,34 @@ | |||
| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |||
There was a problem hiding this comment.
Let's make this example more useful for the user. Maybe copy the example from package:cupertino_http or something.
| /// `true` accepts a certificate that failed normal verification (expired, | ||
| /// self-signed, wrong host, ...). It exists for tests and trusted private | ||
| /// networks - do not use it to accept arbitrary certificates in production. | ||
| class Http2Client extends BaseClient { |
There was a problem hiding this comment.
Maybe marks this experimental for now:
https://api.flutter.dev/flutter/meta/experimental-constant.html
There was a problem hiding this comment.
that makes sense to do since we still have more pending feature work to do per the conformance tests
|
|
||
| List<int>? bodyBytes; | ||
|
|
||
| Future<StreamedResponse> attempt() async { |
There was a problem hiding this comment.
This is awkward to read. Do we need to define this function, which is only used once?
There was a problem hiding this comment.
yeah i agree, it's actually used twice, the initial attempt and then in the .catchError block.
| /// | ||
| /// Capacity is [maxConcurrentOperations], lowered to whatever limit a | ||
| /// resource reports for itself via `concurrencyLimitOf`. | ||
| class ClientPool<T> { |
There was a problem hiding this comment.
Would it be more clear if these were not generic? Isn't the type always going to be ClientConnection for all of these T arguments.
Oh, did you do this to make the tests easier to write?
There was a problem hiding this comment.
yes that's correct, we could also just use MockClientConnection through mockito.
| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
There was a problem hiding this comment.
Maybe provide an example on how to use this library. But I like the approach.
|
|
||
| class _PooledResource<T> { | ||
| _PooledResource(this.future); | ||
| final Future<T> future; |
There was a problem hiding this comment.
I think that these need to be better named/documented:
future refers to the creation of the resource, right?
inFlight refers to the number of concurrent requests of the resource, right?
failed indicates that creating the resource failed, right?
Maybe:
create
inFlightCount
createFailed
And a comment?
This would be more clear if T were ClientConnection but maybe the tests would then be too hard to write.
|
|
||
| while (true) { | ||
| final pooled = _acquire(); | ||
| pooled.inFlight++; |
There was a problem hiding this comment.
Do you need to decrement this if you call _release without using it?
| /// A request counts as in-flight until its response body ends or is | ||
| /// cancelled, so a caller holding a response it never reads will hold this | ||
| /// up. [close] does not await this, so it can never block on that. | ||
| Future<void> terminate() async { |
There was a problem hiding this comment.
Can we just make this part of close for now? I think that we need a general solution for async close?
| /// its most recent SETTINGS_MAX_CONCURRENT_STREAMS (RFC 7540 6.5.2), or | ||
| /// `null` if it hasn't advertised a limit. | ||
| /// | ||
| /// Deliberately not on [ClientTransportConnection]: that class can only be |
There was a problem hiding this comment.
@mosuem maybe we should add it there anyway - there are no "implements ClientTransportConnection" on GitHub but we could also bump semver.
There was a problem hiding this comment.
Yes, just add it there and bump semver.
| } | ||
|
|
||
| final died = Completer<void>(); | ||
| final incoming = socket.transform( |
There was a problem hiding this comment.
Could you add a comment explaining why we need this?
Closes #1385
Adds
Http2Client, a pooled, multiplexedhttp.Clientbacked by HTTP/2 connections, plus the genericClientPool<T>it's built on — fixesdart:io'sHttpClientopening one connection per concurrent request. Ported and generalized from a downstream implementation built for firebase/firebase-admin-dart#305.ClientPool<T>: most-full-first packing, idle GC, failure retirement, awaitable gracefulterminate()Http2Client: pools perhost:port, caps concurrent handshakes globally, multi-host safe (e.g. asgoogleapis_auth'sbaseClient)ClientPooland integration tests forHttp2Clientcovering multi-host pooling, connection-cap dialing, graceful terminate, and the peer-close retry