feat(binding-http): derive ProxyBeginEx from :authority for stream-driven routing#1713
Open
feat(binding-http): derive ProxyBeginEx from :authority for stream-driven routing#1713
Conversation
…iven routing (#1676) Extract :authority and :scheme from HttpBeginExFW headers on new outbound streams, parse host and port (with IPv6 bracket notation and scheme-based defaults), and populate ProxyBeginExFW with INET address and AUTHORITY info entry so that downstream proxy/TLS bindings can route by destination host and perform SNI. https://claude.ai/code/session_0174raBeXFTgt98bp4DTyRDm
jfallows
commented
Apr 21, 2026
Contributor
Author
jfallows
left a comment
There was a problem hiding this comment.
Update at least one spec scripts scenario to verify behavior change for client binding.
Comment on lines
+4695
to
+4703
| final HttpHeaderFW authorityHeader = headers.matchFirst(header -> | ||
| HEADER_AUTHORITY.equals(header.name())); | ||
| final HttpHeaderFW schemeHeader = headers.matchFirst(header -> | ||
| HEADER_SCHEME.equals(header.name())); | ||
|
|
||
| final String authority = authorityHeader != null ? authorityHeader.value().asString() : null; | ||
| final String scheme = schemeHeader != null ? schemeHeader.value().asString() : null; | ||
|
|
||
| client.doNetworkBegin(traceId, authorization, 0, authority, scheme); |
Contributor
Author
There was a problem hiding this comment.
Suggested change
| final HttpHeaderFW authorityHeader = headers.matchFirst(header -> | |
| HEADER_AUTHORITY.equals(header.name())); | |
| final HttpHeaderFW schemeHeader = headers.matchFirst(header -> | |
| HEADER_SCHEME.equals(header.name())); | |
| final String authority = authorityHeader != null ? authorityHeader.value().asString() : null; | |
| final String scheme = schemeHeader != null ? schemeHeader.value().asString() : null; | |
| client.doNetworkBegin(traceId, authorization, 0, authority, scheme); | |
| final HttpHeaderFW authorityHeader = headers.matchFirst(header -> | |
| HEADER_AUTHORITY.equals(header.name())); | |
| final String authority = authorityHeader != null ? authorityHeader.value().asString() : null; | |
| final HttpHeaderFW schemeHeader = headers.matchFirst(header -> | |
| HEADER_SCHEME.equals(header.name())); | |
| final String scheme = schemeHeader != null ? schemeHeader.value().asString() : null; | |
| client.doNetworkBegin(traceId, authorization, 0, authority, scheme); |
Comment on lines
+2622
to
+2664
| if (authority != null && !authority.isEmpty()) | ||
| { | ||
| final String host; | ||
| final int port; | ||
|
|
||
| if (authority.startsWith("[")) | ||
| { | ||
| final int closeBracket = authority.indexOf(']'); | ||
| if (closeBracket != -1 && closeBracket + 1 < authority.length() && authority.charAt(closeBracket + 1) == ':') | ||
| { | ||
| host = authority.substring(1, closeBracket); | ||
| port = parseInt(authority.substring(closeBracket + 2)); | ||
| } | ||
| else | ||
| { | ||
| host = closeBracket != -1 ? authority.substring(1, closeBracket) : authority; | ||
| port = "https".equals(scheme) ? 443 : 80; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| final int colon = authority.lastIndexOf(':'); | ||
| if (colon != -1) | ||
| { | ||
| host = authority.substring(0, colon); | ||
| port = parseInt(authority.substring(colon + 1)); | ||
| } | ||
| else | ||
| { | ||
| host = authority; | ||
| port = "https".equals(scheme) ? 443 : 80; | ||
| } | ||
| } | ||
|
|
||
| extension = proxyBeginExRW.wrap(extBuffer, 0, extBuffer.capacity()) | ||
| .typeId(proxyTypeId) | ||
| .address(a -> a.inet(i -> i.protocol(p -> p.set(STREAM)) | ||
| .source("0.0.0.0") | ||
| .destination(host) | ||
| .sourcePort(0) | ||
| .destinationPort(port))) | ||
| .infos(ii -> ii.item(i -> i.authority(authority))) | ||
| .build(); |
Contributor
Author
There was a problem hiding this comment.
If authority != null, then authority follows the pattern host:port where port is always present. This is the canonical internal format we use to avoid reimplementing port defaulting at each component consuming HttpBeginEx.
Reuse a pattern matcher initially created on "" and reset to current authority to pull out named groups for host and port, where port matches as digits so parsing as an integer will never throw an exception.
Pattern should be simple. not-colon, colon, digits.
…ring conversion Address PR #1713 review feedback: keep each header matchFirst call adjacent to its asString conversion instead of batching the two lookups before both conversions, and break the long IPv6 authority condition across two lines to satisfy the 130-character line limit. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
…med host/port groups Address PR #1713 review feedback: the :authority header on HttpBeginEx is the canonical internal host:port form with port always present, so reuse a pre-compiled pattern matcher (reset against the current authority) to extract named host/port groups instead of reimplementing port defaulting and IPv6 bracket handling at each consumer. Drop the now-unused scheme parameter from doNetworkBegin and the HEADER_SCHEME extraction from onRequestBegin. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
…thority Address PR #1713 review feedback: augment the request.response.and.abort network scripts so the peer accepting the HTTP client binding's outbound connection on zilla://streams/net0 asserts the BEGIN extension carries a ProxyBeginEx with host, port, and authority derived from the application :authority header, and the matching write on the peer-to-peer NetworkIT keeps the two scripts self-consistent. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
… in default client routes When a TCP client has explicit options.host configured and uses DEFAULT_CLIENT_ROUTES, resolve to the configured host/port rather than the incoming ProxyBeginEx address.destination/destinationPort. This restores the pre-#1713 behavior for downstream TCP clients whose target is config-driven, while still allowing a TCP client with no options to derive the destination from beginEx (e.g., Kafka broker resolution). Addresses CI regressions on proxy-style examples (http.proxy, openapi.proxy, grpc.proxy, sse.proxy.jwt, asyncapi.sse.proxy, http.json.schema, http.kafka.avro.json) introduced when HttpClientFactory started emitting ProxyBeginEx.address.inet derived from :authority. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
…:port TLS client uses ProxyBeginEx AUTHORITY info verbatim as the SNI hostname (TlsBindingConfig.newClientEngine L217-222). Passing the raw :authority header value (e.g., "localhost:7143") produces an invalid SNIHostName and breaks the handshake for pipelines like HTTP client → TLS client → TCP client (e.g., examples/http.proxy). Match the codebase convention (see TlsClientFactory L1178 which emits hostname-only authority) by passing the parsed host, not the full :authority header value. The port is still carried on address.destinationPort. Updates the paired request.response.and.abort spec scripts to assert the host-only authority to keep the peer-to-peer IT self-consistent. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
…i unset Align client-side SNI precedence with the sibling ALPN logic (TlsBindingConfig.newClientEngine): an explicitly configured options.sni should win over a dynamically-derived SNI from a ProxyBeginEx AUTHORITY info; fall back to the AUTHORITY info only when options.sni is null. Previously the AUTHORITY info unconditionally overrode configured options.sni. This was a latent bug that broke proxy-style examples (e.g. examples/http.proxy) as soon as an upstream binding started emitting ProxyBeginEx with AUTHORITY info (HTTP client derivation from :authority introduced by #1676): the TLS client would replace its configured SNI (e.g. [nginx]) with the client's request authority (e.g. [localhost]), presenting the wrong SNI to the upstream server and failing the handshake. https://claude.ai/code/session_01GS1do8LS5Av6wrFTTduMVK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
:authorityand:schemeheaders fromHttpBeginExFWwhen opening a new outbound network stream inHttpClientFactoryhostandportwith full IPv6 bracket-notation support (e.g.[::1]:8443) and scheme-based port defaults (443 forhttps, 80 forhttp)ProxyBeginExFWwithaddress.inet(family=INET, protocol=STREAM, source=0.0.0.0, destination=host, sourcePort=0, destinationPort=port) and aninfosentry of type AUTHORITY carrying the full:authorityvalue for TLS SNIEMPTY_OCTETSwhen no authority is present, preserving existing behaviourproxyBeginExRWis a non-static field onHttpClientFactory, reused across all streams on the worker threadChanges
runtime/binding-http/…/stream/HttpClientFactory.javaProxyAddressProtocol.STREAMProxyBeginExFW.Builder proxyBeginExRWString8FW HEADER_SCHEMEHttpClient.doNetworkBegin— gainsauthorityandschemeparameters; buildsProxyBeginExFWwhen authority is non-emptyHttpExchange.onRequestBegin— extracts:authorityand:schemefrom headers before callingdoNetworkBeginTest plan
runtime/binding-httpsuccessfully./mvnw test -pl runtime/binding-http -DskipITs— existing unit tests pass./mvnw verify -pl specs/binding-http.specProxyBeginExFWis populated correctly for plain-host, host:port, and IPv6 authoritiesCloses #1676