Skip to content

Allow omitting the Content-Length header on unary requests - #439

Open
sevlaan wants to merge 1 commit into
connectrpc:mainfrom
sevlaan:optional-content-length
Open

sevlaan wants to merge 1 commit into
connectrpc:mainfrom
sevlaan:optional-content-length

Conversation

@sevlaan

@sevlaan sevlaan commented Sep 15, 2026

Copy link
Copy Markdown

Fixes #438.

ConnectUnaryChannelHandler sets a Content-Length header on every unary request sent through NIOHTTPClient. NIOHTTPClient speaks HTTP/2 only, where the header is optional: DATA frames are self-delimiting and END_STREAM marks the end of the body. grpc-go and grpc-swift do not send it.

It becomes a problem behind a gRPC proxy that forwards incoming metadata to its own outgoing calls (a common way to propagate auth and tracing headers). Content-Length is not on grpc-go's reserved-header list, so it is forwarded too, and the next hop gets a length that does not match its own body. Envoy then resets the stream with PROTOCOL_ERROR before the handler runs. Moving a client from grpc-swift to connect-swift is enough to trigger this, with no server change.

There is no way to suppress the header from the application today: it is added below the interceptor chain, and the only hook above it is subclassing NIOHTTPClient, which is not viable across a module boundary.

Change

A nested NIOHTTPClient.ContentLength option, passed as the last initializer parameter:

public init(
    host: String,
    port: Int? = nil,
    timeout: TimeInterval? = nil,
    contentLength: ContentLength = .sent
)

.sent is the default and keeps today's behaviour, so nothing changes for existing users. It follows the shape of the existing ProtocolClientConfig.UnaryGET option.

Tests

ConnectNIO had no unit test target, so this adds one: ConnectLibraryTests now also depends on ConnectNIO (one line in Package.swift), and ConnectUnaryChannelHandlerTests drives the handler with an EmbeddedChannel — no server or sockets. Three cases: the header is present by default, absent with .omitted, and Host plus caller headers survive in both modes.

swift build and the new tests pass locally, and swiftlint lint is clean. I could not run make testconformance here (no Go toolchain on this machine), so I am relying on CI for it — the default path is unchanged, so I do not expect any movement there.

Happy to switch to a plain Bool or another spelling if you prefer.

🤖 Generated with Claude Code

NIOHTTPClient always sets a Content-Length header on unary requests. The
header is optional over HTTP/2, which is the only protocol this client
speaks, and neither grpc-go nor grpc-swift send it.

It causes trouble behind a gRPC proxy that forwards incoming metadata to
its own outgoing calls: Content-Length is not on grpc-go's reserved-header
list, so the forwarded length is applied to a different body and the next
hop resets the stream with PROTOCOL_ERROR. There is no way to suppress the
header from application code today, because it is added below the
interceptor chain.

Add a ContentLength option to NIOHTTPClient, defaulting to .sent so that
existing behaviour is unchanged, and cover the handler with the first unit
tests for the NIO transport.

Signed-off-by: Vsevolod Bespalov <vsevolod.bespalov@plata.work>
@sevlaan
sevlaan force-pushed the optional-content-length branch from fe7db77 to d320036 Compare September 15, 2026 12:57
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.

Make the Content-Length header optional in NIOHTTPClient

1 participant