Skip to content

UDS resolver falls back to URL-encoded socket path for :authority, breaking tonic interop #176

Description

@mitchellTsukaeru

What

When HTTP2ClientTransport.Posix is constructed with a .unixDomainSocket(path:) target and no explicit config.http2.authority, the :authority pseudo-header is set to the URL-encoded socket path. Servers that strictly validate :authority per RFC 7540 reject the request with RST_STREAM(PROTOCOL_ERROR).

The most common impact is interop with Rust tonic gRPC servers — its underlying hyperium/h2 layer rejects malformed authority values. Surface error in Swift:

unavailable: Stream unexpectedly closed: received RST_STREAM frame (0x1: protocol error)

Reproduction

A grpc-swift-nio-transport client targeting .unixDomainSocket(path:) without overriding the authority will emit something like:

:authority: %2Ftmp%2Fmy-socket.sock

That value violates RFC 7540 §8.1.2.3, which constrains :authority to [userinfo@]host[:port] — a percent-encoded filesystem path is not a valid form.

Root cause

NameResolver+UDS.swift:76:

authority: target.authority ?? target.address.path

The fallback is the socket path. That string then flows into PercentEncoding.encodeAuthority(...) at GRPCChannel.swift:101 and is emitted as the pseudo-header value.

What other implementations do

The grpc-swift server-side doesn't validate :authority content, so Swift-client → Swift-server happens to work — but Swift-client → tonic-server (or any RFC-7540-strict server) fails.

Workaround

Setting config.http2.authority = "localhost" overrides the resolver fallback and unblocks interop:

var config = HTTP2ClientTransport.Posix.Config.defaults
config.http2.authority = \"localhost\"
let transport = try HTTP2ClientTransport.Posix(
    target: .unixDomainSocket(path: socketPath),
    transportSecurity: .plaintext,
    config: config,
)

Suggested fix

For UDS targets where the caller hasn't supplied an authority, default to \"localhost\" to match the convention adopted across the gRPC ecosystem. Concretely in NameResolvers.UnixDomainSocket.resolver(for:):

- authority: target.authority ?? target.address.path
+ authority: target.authority ?? \"localhost\"

This is also consistent with the ecosystem expectation that :authority is not used for routing on UDS — the address itself selects the destination.

Happy to send a PR if helpful.

Versions

  • grpc-swift-nio-transport: 2.7.0 (also confirmed on main HEAD as of 2026-04-28)
  • grpc-swift-2: 2.4.0
  • Swift: 6.x / Xcode 26.4
  • macOS 25.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions