Skip to content

Add unix domain socket support for bind targets - #96

Open
mob-connection wants to merge 6 commits into
swift-server:mainfrom
mob-connection:uds-bind-target
Open

Add unix domain socket support for bind targets#96
mob-connection wants to merge 6 commits into
swift-server:mainfrom
mob-connection:uds-bind-target

Conversation

@mob-connection

@mob-connection mob-connection commented Jul 13, 2026

Copy link
Copy Markdown

Motivation

NIOHTTPServer could only bind to a host and port. Serving over a UNIX domain socket is a common requirement (e.g. a reverse proxy talking to the server over a local socket), so this PR adds a UDS bind target.

Modifications

• Added BindTarget.unixDomainSocket(path:) (typed as System.FilePath) and a matching SocketAddress case; the path is converted to String only at the NIO edge (bind(unixDomainSocketPath:) and fileIO.unlink).
• Bind over a UNIX domain socket for both plaintext (HTTP/1.1) and secure-upgrade (HTTP/1.1 + HTTP/2) channels, and report the bound path from listeningAddresses.
• Remove the socket file on shutdown; fail the bind if the path is already occupied so a stale socket is never silently reused.
• Support a socketPath key in swift-configuration, mutually exclusive with host/port.
• Reject UNIX domain socket bind targets when HTTP/3 is enabled (HTTP/3 runs over QUIC/UDP), with a dedicated configuration error.
• Added tests for the listening-address mapping and the HTTP/3 rejection.
• Factored the bind-target dispatch into a ServerBootstrap.bind(_:to:childChannelInitializer:) helper so the host/port and UDS paths aren't duplicated across the HTTP/1.1 and secure-upgrade setup.

Result

NIOHTTPServer can now listen on a UNIX domain socket over HTTP/1.1 and HTTP/2. Resolves #69.

@mob-connection

Copy link
Copy Markdown
Author

For #69 I used a struct-backed NIOHTTPServer.SocketAddress with an added
.unixDomainSocket(path:) case, so it stays extensible (e.g. vsock later)
without a source-breaking change.

Do you think it makes sense to expose it as a plain enum instead ?

/// ```swift
/// let target = BindTarget.unixDomainSocket(path: "/tmp/server.sock")
/// ```
public static func unixDomainSocket(path: String) -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use the new FilePath type once it becomes available

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use the new FilePath type once it becomes available

In the UDS bind support the socket path has two different types depending on direction: on input we take a FilePath (BindTarget.unixDomainSocket(path:)), but on output we expose it as String (SocketAddress.unixDomainSocketPath, mirroring NIOCore.SocketAddress.pathname). If we commit to FilePath, I'd lean toward using it on output too for consistency — but keeping String also makes sense since that's just what NIO reports back. Which way should we go?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think ignore what NIO does and keep the API what looks the best in Swift, in this case FilePaths for all types that represent instead of a String

Adds a `.unixDomainSocket(path:)` bind target so the server can listen on a
UNIX domain socket in addition to host and port.

- Add `BindTarget.unixDomainSocket(path:)` and a matching `SocketAddress` case
- Bind via `ServerBootstrap.bind(unixDomainSocketPath:)` for both plaintext and
  secure-upgrade channels, and report the bound path from `listeningAddresses`
- Remove the socket file on shutdown; fail the bind if the path is already
  occupied so a stale socket is never silently reused
- Support a `socketPath` key in swift-configuration, mutually exclusive with
  `host`/`port`

Resolves swift-server#69
Motivation:

The UDS bind path was typed as a plain String. A System.FilePath gives
it a strongly-typed, path-aware API at the configuration boundary.

Modifications:

- BindTarget.unixDomainSocket(path:) and its backing now take a FilePath
- Convert to String only at the NIO edge (bind(unixDomainSocketPath:)
  and fileIO.unlink), where NIO still requires a String
- public import System where FilePath appears in public API; plain
  import elsewhere
- Update UDS tests to pass a FilePath

Result:

The UDS bind path is strongly typed; string literals still work via
ExpressibleByStringLiteral, so call sites are unchanged.
Motivation:

SocketAddress.init(_:) folded address, port and pathname lookups into a
single tuple switch with coarse error cases, making failure reasons
ambiguous and leaving the mapping untested.

Modifications:

- Replace addressOrPortNotAvailable/unsupportedAddressType with the more
  precise addressNotAvailable and portNotAvailable cases
- Rebuild init(_:) around per-field throwing accessors and an exhaustive
  switch over the address kind
- Switch the NIOClient test helper over SocketAddress.base instead of the
  optional host/port/path accessors
- Add SocketAddressTests covering the IPv4, IPv6, UDS and nil mappings

Result:

Listening-address construction has precise error reasons and unit-test
coverage; behaviour is unchanged.
Switch connectToTestSecureUpgradeHTTPServer over SocketAddress.base like
the HTTP/1.1 helper, and drop the now-unused TestError.unsupportedAddress.
@mob-connection
mob-connection marked this pull request as ready for review July 18, 2026 13:49

@0xTim 0xTim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking mostly good, I think we should add H2 support as well though

… HTTP/3

Merges swift-server#101 and rejects unix domain socket bind targets when HTTP/3 is enabled, since HTTP/3 runs over QUIC/UDP.
)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
case .unixDomainSocket(let path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid duplication with the case above, we should define an extension on ServerBootstrap's bind method that accepts a NIOHTTPServerConfiguration.BindTarget. That method should switch over the bind target and call into either the host and port variant or the UDS variant of ServerBootstrap.bind.

)
}
serverChannels.append((serverChannel, serverQuiescingHelper))
case .unixDomainSocket(let path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. You will be able to use the extension here too.

Resolve test-client conflicts, fix HTTP/3 test client for optional SocketAddress host/port, and dedup ServerBootstrap.bind via a BindTarget-aware helper.

// Remove the socket files for any UDS bind targets so their paths are freed for the next run.
// Registered only after all binds succeeded, so every path is one we created.
defer { await self.removeUNIXDomainSocketFiles() }

@mob-connection mob-connection Aug 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serve(...) now has two defer blocks — finishListeningAddressPromise() and await removeUNIXDomainSocketFiles(). Just flagging that for visibility in case it's worth consolidating or reordering; happy to adjust if you'd prefer a different structure. 🙌

@mob-connection

Copy link
Copy Markdown
Author

Deduplicated the bind logic (@aryan-25's comments): added a ServerBootstrap.bind(_:to:childChannelInitializer:) helper that switches over the BindTarget and dispatches to the host/port or UDS overload, so setupHTTP1_1ServerChannels and setupSecureUpgradeServerChannels no longer duplicate the setup. One note: it's a static method taking sending ServerBootstrap — under strict concurrency self.bind on the non-Sendable bootstrap trips "sending 'self' risks data races", and self can't be marked sending.
• Merged latest main and resolved the conflicts; all tests pass locally.

Left one small note inline about the two defer blocks in serve(...). Happy to iterate further — thanks again! 🙌

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.

Support binding to a unix socket

4 participants