Skip to content

Adopt apple/swift-http-types for the HTTP transport#135

Merged
odrobnik merged 3 commits into
mainfrom
claude/crazy-gates-4f3576
Jun 10, 2026
Merged

Adopt apple/swift-http-types for the HTTP transport#135
odrobnik merged 3 commits into
mainfrom
claude/crazy-gates-4f3576

Conversation

@odrobnik

@odrobnik odrobnik commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Evaluates and adopts apple/swift-http-types to harden the HTTP transport, in two reviewable phases. The win is concentrated in the header/method/status currency types; the SSE/streaming/IO machinery is untouched.

Why (robustness)

The HTTP layer previously carried headers as [(String, String)] with a custom RouteMethod enum and HTTPStatus struct, and three different header representations (NIO HTTPHeaders on the wire, [(String, String)] in routing, Foundation header dicts on the client) with hand-written conversions between them.

Replacing that with HTTPFields / HTTPRequest.Method / HTTPResponse.Status:

  • Case-insensitivity is library-provided instead of hand-rolled caseInsensitiveCompare re-implemented in the route accessor, the OAuth proxy, and the header test.
  • Response defaults can no longer duplicate a headerHTTPFields replaces by name rather than appending. The regression test for this is now structural.
  • Header names/values are validated at construction (RFC 9110 tokens, CRLF sanitisation). Most relevant for the OAuth transparent proxy, which copies client-supplied and upstream headers in both directions — copyUpstreamHeaders now drops invalid upstream field names instead of forwarding them blind.
  • MCP header names are typed, shared constants (.mcpSessionID, .mcpProtocolVersion, .lastEventID) used by both the server routing layer and the client, instead of scattered string literals a typo would silently break.

HTTPTypes is zero-dependency and Foundation-free, so it lives in the core target (not behind the Server trait) and is shared by client and server. The client-only / NIO-free build stays NIO-free (verified).

Phase 1 — a57eff2 — currency types

  • HTTPRouteRequest / HTTPRouteResponse now expose headerFields: HTTPFields; RouteResponse, the router, and all built-in routes migrated.
  • RouteMethodHTTPRequest.Method; HTTPStatusHTTPResponse.Status (both deleted).
  • Client (MCPServerProxy) sets/reads the MCP headers through the shared HTTPField.Name constants via small typed URLRequest/HTTPURLResponse helpers; the transfer still goes through URLSession / SwiftCross.
  • HTTPHandler keeps a manual NIO↔http-types conversion for this phase.

Phase 2 — 1d3bc7f — NIO interop

  • Inserts swift-nio-extras' HTTP1ToHTTPServerCodec into the pipeline (after HTTPLogger, before HTTPHandler), so HTTPHandler works in HTTPRequest/HTTPResponse/HTTPFields directly.
  • Deletes the bespoke convertMethod / convertHeaders / nioStatus / nioHeaders(from:) glue. HTTPLogger stays on the NIO side of the codec, unchanged.
  • The codec lifts Host into :authority; requestHeaderFields(from:) re-exposes it so routes reading header("Host") keep working.

Breaking changes (public API)

  • addRoute(_:_:) takes HTTPRequest.Method.post instead of .POST.
  • HTTPRouteRequest.headers / HTTPRouteResponse.headers ([(String, String)]) and HTTPRouteResponse(status:headers:body:) remain as deprecated shims over headerFields; header(_:), bearerToken, sessionID are unchanged.
  • HTTPStatusHTTPResponse.Status, RouteMethodHTTPRequest.Method.

Dependencies

  • apple/swift-http-types from: 1.0.0 (core, unconditional — zero-dep, Foundation-free).
  • apple/swift-nio-extras from: 1.25.0 (NIOHTTPTypes + NIOHTTPTypesHTTP1, Server trait only).

Verification

  • swift build ✓ and full swift test ✓ (421 tests, 70 suites) after each phase.
  • swift build --disable-default-traits --traits Client ✓ — client-only build stays NIO-free.

🤖 Generated with Claude Code

odrobnik and others added 3 commits June 9, 2026 22:58
Replace the stringly-typed `[(String, String)]` header model, the custom
`RouteMethod` enum, and the `HTTPStatus` struct with Apple's
swift-http-types — `HTTPFields`, `HTTPRequest.Method`, and
`HTTPResponse.Status` — across the HTTP transport.

Why this improves robustness:
- Header lookups are now case-insensitive and validated by the library
  instead of hand-rolled `caseInsensitiveCompare` re-implemented in three
  places (route request accessor, OAuth proxy, header test).
- Response defaults can no longer duplicate a header: `HTTPFields` replaces
  by name rather than appending (the regression test for this is now
  structural, not behavioural).
- Header names/values are validated at construction (RFC 9110 tokens, CRLF
  sanitisation) — most relevant for the OAuth proxy, which copies
  client-supplied and upstream headers in both directions.
- MCP-specific header names (`Mcp-Session-Id`, `MCP-Protocol-Version`,
  `Last-Event-ID`) are now shared, typed `HTTPField.Name` constants used by
  both the server routing layer and the client, instead of scattered string
  literals a typo would silently break.

`HTTPTypes` is zero-dependency and Foundation-free, so it is linked into the
core target (not behind the `Server` trait) and shared by client and server.
`HTTPHandler` keeps a manual NIO<->http-types conversion for now; Phase 2
replaces it with the NIOHTTPTypesHTTP1 codec.

Public API: `HTTPRouteRequest`/`HTTPRouteResponse` now expose `headerFields`
(`HTTPFields`); the old `headers: [(String, String)]` and the
`HTTPRouteResponse(status:headers:body:)` initializer remain as deprecated
shims. `addRoute(_:_:)` takes `HTTPRequest.Method` (`.post` instead of
`.POST`); `HTTPStatus` becomes `HTTPResponse.Status`.

All 421 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase 2)

Insert swift-nio-extras' `HTTP1ToHTTPServerCodec` into the channel pipeline
(after `HTTPLogger`, before `HTTPHandler`) so `HTTPHandler` consumes
`HTTPRequestPart`/`HTTPResponsePart` carrying `HTTPRequest`/`HTTPResponse`
directly. The handler no longer translates between NIO and http-types by
hand: `convertMethod`, `convertHeaders`, `nioStatus`, and `nioHeaders(from:)`
are all deleted, and `RequestState` now carries an `HTTPRequest`.

`HTTPLogger` stays on the NIO side of the codec, so its request/response
logging is unchanged. The one direct-to-channel SSE write path
(`Channel.sendSSE`) now emits an `HTTPResponsePart` so it round-trips through
the codec like every other write.

The HTTP/1 codec lifts the `Host` header into the request's `:authority`
pseudo-header; `HTTPHandler.requestHeaderFields(from:)` re-exposes it as a
`Host` field so routes that read `header("Host")` (OAuth redirect/issuer URL
construction, legacy SSE) keep working.

`NIOHTTPTypes`/`NIOHTTPTypesHTTP1` are linked only under the `Server` trait;
the client-only / NIO-free build is unaffected.

All 421 tests pass; the client-only (Server-disabled) build stays NIO-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The HTTPStatus -> HTTPResponse.Status rename in Phase 1 pushed the `json`
factory signature to 123 chars; wrap the parameters onto separate lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@odrobnik odrobnik merged commit 09ed72f into main Jun 10, 2026
7 checks passed
@odrobnik odrobnik deleted the claude/crazy-gates-4f3576 branch June 10, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant