Repository navigation
Conversation
…estCustomizer The JDK's HttpClient has no default headers, so neither WebSocketAcpClientTransport nor StreamableHttpAcpClientTransport could send an Authorization header, an API key or any other header an agent's endpoint requires; an application had to write its own transport. goose serve, for one, refuses every connection without X-Secret-Key. - WebSocketAcpClientTransport.webSocketCustomizer(Consumer<WebSocket.Builder>) runs on every connect attempt, after the connect timeout is set. A customizer that throws, or sets a header the JDK reserves for the handshake, fails that connect, which may be tried again. - StreamableHttpAcpClientTransport.requestCustomizer(Consumer<HttpRequest.Builder>) runs for every request the transport sends: the cleartext probe, initialize, each POST, every SSE stream opened or reopened, and the closing DELETE, so a token that expires is read again for each. It runs on a builder of its own and the result is copied with the protocol headers (Content-Type, Accept, Acp-Connection-Id, Acp-Session-Id) filtered out and the endpoint's URI restored, because a builder can replace a header but not remove one: the bootstrap initialize carries no connection id of the transport's own to replace a customizer's with. - Each HTTP request is now built inside its Mono, so a customizer that throws fails that Mono rather than escaping sendMessage, and a failed initialize may be sent again. initialize is built after the cleartext probe instead of being re-stamped with the settled version, so StreamableHttpRequests.pinned(HttpRequest) is gone. Verified against goose serve 1.52.0: without the header the WebSocket handshake is refused and initialize over HTTP answers 401; with X-Secret-Key from either customizer, initialize and session/new succeed.
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.
Why
The JDK's
HttpClienthas no default headers, so neitherWebSocketAcpClientTransportnorStreamableHttpAcpClientTransportcan send anAuthorizationheader, an API key, or any other header an agent's endpoint requires. Today an application that talks to an authenticated agent has to write its own transport.goose serveis one such agent: it refuses every connection withoutX-Secret-Key(the alternative is--dangerously-unauthenticated).What
webSocketCustomizer(Consumer<WebSocket.Builder>)runs on every connect attempt, after the connect timeout is set.requestCustomizer(Consumer<HttpRequest.Builder>)runs for every request: the cleartext probe,initialize, each POST, every SSE stream opened or reopened, and the closingDELETE. A token that expires is therefore read again for each.HttpRequest.newBuilder(request, filter): Content-Type, Accept,Acp-Connection-IdandAcp-Session-Idare filtered out and the endpoint's URI is restored. A copy is needed because a builder can replace a header but never remove one, and the bootstrapinitializehas no connection id of the transport's own to replace a customizer's with. Other settings, such as a per-request timeout, are kept.Mono, so a customizer that throws, or a header the JDK restricts, fails that request instead of escapingsendMessage, and a failedinitializemay be sent again. The WebSocket connect behaves the same way and may be retried.initializeis built after the cleartext probe rather than re-stamped with the settled version, so the package-privateStreamableHttpRequests.pinned(HttpRequest)is gone.The shape follows the
customizeRequesthook of the MCP Java SDK's HTTP client transport.Verification
./mvnw clean verify: all 16 modules, 2,062 tests, 0 failures (JDK 21, Error Prone / NullAway profile).WebSocketAcpClientTransportTest: a real handshake against the JDK'sHttpServer, which records theAuthorizationheader and answers 401; a throwing customizer fails the connect, which can then be retried; a reservedSec-WebSocket-*header fails the connect; null is rejected.StreamableHttpAcpClientTransportTest: across POST, GET, POST and DELETE, every request carries the header once; a changed token reaches the next request; a customizer'sAcp-Connection-IdandAcceptare dropped, including on the bootstrap POST; a throwing customizer fails theMonoandinitializecan be retried; null is rejected.goose serve1.52.0, both transports: without the header, the WebSocket handshake is refused andinitializeover HTTP gets 401. WithX-Secret-Keyset through either customizer,initializeandsession/newsucceed.CHANGELOG entry under
[Unreleased]→ Added.🤖 Generated with Claude Code