Summary
HttpClient.canServe has an HTTP/2 arm intended to let one connection serve several authorities:
private boolean canServe(
String scheme,
String authority)
{
return Objects.equals(this.scheme, scheme) &&
(Objects.equals(this.authority, authority) ||
encoder == HttpEncoder.HTTP_2 && serviceableNames.contains(authority));
}
serviceableNames has exactly two references in the tree — the declaration at
HttpClientFactory:2282 and this read at :4441. Nothing ever adds to it, and as a private field of
a private inner class nothing outside that file can. So the disjunct is always false and canServe
reduces to exact (scheme, authority) equality.
The effect is conservative — more connections than necessary, never a connection used for the wrong
origin — so this is not a correctness bug. It is a feature that reads as implemented but is not.
Why it is worth fixing rather than deleting
The distinction the field is reaching for is a real protocol property, not a policy preference:
- HTTP/1.1 — the connection is to one origin and
Host must match the target authority. Exactly
one origin per connection, by construction.
- HTTP/2 (§9.1.1, Connection Reuse) — a connection MAY carry requests for multiple different
URI authority components, provided the connection is authoritative for the new authority. For
https that means the peer certificate is valid for it.
So the general model is: origins per connection is a function of the protocol version — HTTP/1.1
exactly one, HTTP/2 an authoritative set. That is worth stating explicitly in the pool rather than as
an inline disjunct, and it is the shape serviceableNames already has.
Note the existing arm gets one thing right that should survive: it tests encoder == HTTP_2 and so
excludes H2C. Cleartext has no certificate to establish authority, so coalescing there would rest on
nothing but DNS. Keep that exclusion.
Where the authority signal can come from
The signal is already carried on an existing path, in the right direction, by the component that did
the verification.
TlsClientFactory.onDecodeHandshakeFinished reads the completed handshake and hands the result up to
the application stream:
final String protocol = tlsEngine.getApplicationProtocol();
ExtendedSSLSession session = (ExtendedSSLSession) tlsEngine.getSession();
List<SNIServerName> serverNames = session.getRequestedServerNames();
...
doAppBegin(traceId, budgetId, hostname, protocol);
and doAppBegin (TlsClientFactory:1157-1183) emits a reply ProxyBeginEx carrying alpn(protocol)
and authority(hostname). binding-http already depends on this path — it is how the client learns
the negotiated ALPN, which selects the encoder. The reverse direction is wired too:
HttpClientFactory:2610 sends .infos(ii -> ii.item(i -> i.authority(host))) down, and
TlsBindingConfig.newClientEngine consumes AUTHORITY as SNI (line 305), ALPN as the protocol list
(317), and SECURE/NAME into the SSLSession as COMMON_NAME_KEY (354-363).
So this needs no new extension type and no new cross-binding contract — only additional info items on
an extension already emitted, at a call site that already holds the SSLSession and therefore
getPeerCertificates().
Two constraints on doing it:
-
What flows up today is the SNI we asked for, not what the peer proved.
getRequestedServerNames() is our own request echoed back; it authorizes nothing. The feature needs
SAN dNSNames from the presented chain.
-
Hostname verification is conditional. TlsBindingConfig:328:
if (clientHttpsIdentification)
{
parameters.setEndpointIdentificationAlgorithm("HTTPS");
}
With that off the engine is not matching certificate against hostname at all, so names read off the
chain are not validated in any useful sense and coalescing on them would be unsound. The emitted
names must be ones that passed trust verification — never raw SAN contents — which means either
requiring that property or doing the name matching explicitly.
Precondition: client-side 421 retry
HTTP/2 §9.1.2 — a server that does not consider itself authoritative for a coalesced request answers
421 Misdirected Request, and the client is expected to retry on a fresh connection to the requested
origin.
Today 421 exists only as a reason-phrase byte array in HttpServerFactory's status table (the
encoding side). There is no client-side handling. Coalescing without 421 retry converts a server's
legitimate "not authoritative for that" into a user-visible 421 — reintroducing the mis-routed-request
class of failure that #1811 removed, through a different door.
So 421 retry is a gate on this feature, not a follow-on to it.
Encoding decision
ProxySecureInfo NAME is string16 singular — PP2_SUBTYPE_SSL_CN, a Common Name rather than a SAN
list — and is already spoken for in the downward direction as COMMON_NAME_KEY. ProxyInfo[] infos
is repeatable, so repeated AUTHORITY items is the zero-.idl-change option, and there is already a
// TODO: support multiple authority info at TlsBindingConfig:307 (with a matching one for ALPN at
319) sitting on that seam.
But it overloads the type: downward AUTHORITY means "the SNI to use", upward it would mean "names
this peer is authoritative for", and every current consumer reads it with matchFirst. A distinct
ProxyInfoType variant keeps the semantics clean at the cost of an .idl change and a full build.
Worth deciding before implementation.
Alternative signal, for later
RFC 8336's ORIGIN HTTP/2 frame (type 0xc, absent from Http2FrameType) lets a server declare its
origin set. It is a reasonable later addition but not the primary mechanism: it narrows what a server
says it will serve without relaxing the certificate requirement that §9.1.1 actually turns on, and it
may never arrive, whereas certificate names are available at handshake completion — before the first
HTTP/2 frame — so a pool decision can be made at connection setup.
Relationship to #2339
Deliberately not part of #2339. That issue rescopes connection capacity from per-route to
per-origin, and it should land with canServe unchanged and exact-matching.
The two do interact, in one direction worth recording: a connection that serves an origin set
belongs to several origin allowances at once. Provided #2339 implements per-origin capacity by
counting through canServe rather than by keying a map per origin (see
#2339 (comment)), coalescing composes with it
without rework — a coalescable connection simply counts toward each allowance it can serve, which is
the conservative and correct reading of a shared resource.
Also relevant to #2339's ceiling: because this arm is inert, HTTP/2 coalescing currently provides no
relief for the per-route connection bound even among origins sharing a certificate.
Scope
- Emit validated peer names from
binding-tls on the existing reply ProxyBeginEx, gated on
hostname verification actually being in force.
- Populate
serviceableNames from them in HttpClientFactory, HTTP/2 only, H2C still excluded.
- Client-side
421 Misdirected Request retry on a fresh connection — required before (2) is safe to
enable.
- Spec scripts for: coalesced request served on an existing connection; a
421 answer retried on a
fresh connection; H2C never coalescing; and coalescing declined when hostname verification is
disabled.
Until then, the dead disjunct is a trap for the next reader of canServe — particularly once #2339
makes the capacity model depend on that predicate. Either remove it as part of #2339 and reintroduce
it here with a real signal behind it, or leave it with a comment pointing at this issue.
Summary
HttpClient.canServehas an HTTP/2 arm intended to let one connection serve several authorities:serviceableNameshas exactly two references in the tree — the declaration atHttpClientFactory:2282and this read at:4441. Nothing ever adds to it, and as a private field ofa private inner class nothing outside that file can. So the disjunct is always false and
canServereduces to exact
(scheme, authority)equality.The effect is conservative — more connections than necessary, never a connection used for the wrong
origin — so this is not a correctness bug. It is a feature that reads as implemented but is not.
Why it is worth fixing rather than deleting
The distinction the field is reaching for is a real protocol property, not a policy preference:
Hostmust match the target authority. Exactlyone origin per connection, by construction.
URI authority components, provided the connection is authoritative for the new authority. For
httpsthat means the peer certificate is valid for it.So the general model is: origins per connection is a function of the protocol version — HTTP/1.1
exactly one, HTTP/2 an authoritative set. That is worth stating explicitly in the pool rather than as
an inline disjunct, and it is the shape
serviceableNamesalready has.Note the existing arm gets one thing right that should survive: it tests
encoder == HTTP_2and soexcludes
H2C. Cleartext has no certificate to establish authority, so coalescing there would rest onnothing but DNS. Keep that exclusion.
Where the authority signal can come from
The signal is already carried on an existing path, in the right direction, by the component that did
the verification.
TlsClientFactory.onDecodeHandshakeFinishedreads the completed handshake and hands the result up tothe application stream:
and
doAppBegin(TlsClientFactory:1157-1183) emits a replyProxyBeginExcarryingalpn(protocol)and
authority(hostname).binding-httpalready depends on this path — it is how the client learnsthe negotiated ALPN, which selects the encoder. The reverse direction is wired too:
HttpClientFactory:2610sends.infos(ii -> ii.item(i -> i.authority(host)))down, andTlsBindingConfig.newClientEngineconsumesAUTHORITYas SNI (line 305),ALPNas the protocol list(317), and
SECURE/NAMEinto theSSLSessionasCOMMON_NAME_KEY(354-363).So this needs no new extension type and no new cross-binding contract — only additional info items on
an extension already emitted, at a call site that already holds the
SSLSessionand thereforegetPeerCertificates().Two constraints on doing it:
What flows up today is the SNI we asked for, not what the peer proved.
getRequestedServerNames()is our own request echoed back; it authorizes nothing. The feature needsSAN
dNSNames from the presented chain.Hostname verification is conditional.
TlsBindingConfig:328:With that off the engine is not matching certificate against hostname at all, so names read off the
chain are not validated in any useful sense and coalescing on them would be unsound. The emitted
names must be ones that passed trust verification — never raw SAN contents — which means either
requiring that property or doing the name matching explicitly.
Precondition: client-side 421 retry
HTTP/2 §9.1.2 — a server that does not consider itself authoritative for a coalesced request answers
421 Misdirected Request, and the client is expected to retry on a fresh connection to the requestedorigin.
Today
421exists only as a reason-phrase byte array inHttpServerFactory's status table (theencoding side). There is no client-side handling. Coalescing without 421 retry converts a server's
legitimate "not authoritative for that" into a user-visible 421 — reintroducing the mis-routed-request
class of failure that #1811 removed, through a different door.
So 421 retry is a gate on this feature, not a follow-on to it.
Encoding decision
ProxySecureInfo NAMEisstring16singular — PP2_SUBTYPE_SSL_CN, a Common Name rather than a SANlist — and is already spoken for in the downward direction as
COMMON_NAME_KEY.ProxyInfo[] infosis repeatable, so repeated
AUTHORITYitems is the zero-.idl-change option, and there is already a// TODO: support multiple authority infoatTlsBindingConfig:307(with a matching one for ALPN at319) sitting on that seam.
But it overloads the type: downward
AUTHORITYmeans "the SNI to use", upward it would mean "namesthis peer is authoritative for", and every current consumer reads it with
matchFirst. A distinctProxyInfoTypevariant keeps the semantics clean at the cost of an.idlchange and a full build.Worth deciding before implementation.
Alternative signal, for later
RFC 8336's
ORIGINHTTP/2 frame (type0xc, absent fromHttp2FrameType) lets a server declare itsorigin set. It is a reasonable later addition but not the primary mechanism: it narrows what a server
says it will serve without relaxing the certificate requirement that §9.1.1 actually turns on, and it
may never arrive, whereas certificate names are available at handshake completion — before the first
HTTP/2 frame — so a pool decision can be made at connection setup.
Relationship to #2339
Deliberately not part of #2339. That issue rescopes connection capacity from per-route to
per-origin, and it should land with
canServeunchanged and exact-matching.The two do interact, in one direction worth recording: a connection that serves an origin set
belongs to several origin allowances at once. Provided #2339 implements per-origin capacity by
counting through
canServerather than by keying a map per origin (see#2339 (comment)), coalescing composes with it
without rework — a coalescable connection simply counts toward each allowance it can serve, which is
the conservative and correct reading of a shared resource.
Also relevant to #2339's ceiling: because this arm is inert, HTTP/2 coalescing currently provides no
relief for the per-route connection bound even among origins sharing a certificate.
Scope
binding-tlson the existing replyProxyBeginEx, gated onhostname verification actually being in force.
serviceableNamesfrom them inHttpClientFactory, HTTP/2 only,H2Cstill excluded.421 Misdirected Requestretry on a fresh connection — required before (2) is safe toenable.
421answer retried on afresh connection;
H2Cnever coalescing; and coalescing declined when hostname verification isdisabled.
Until then, the dead disjunct is a trap for the next reader of
canServe— particularly once #2339makes the capacity model depend on that predicate. Either remove it as part of #2339 and reintroduce
it here with a real signal behind it, or leave it with a comment pointing at this issue.