Skip to content

Validate configuration at initialization - #107

Open
aryan-25 wants to merge 17 commits into
swift-server:mainfrom
aryan-25:validate-config-at-init
Open

Validate configuration at initialization#107
aryan-25 wants to merge 17 commits into
swift-server:mainfrom
aryan-25:validate-config-at-init

Conversation

@aryan-25

@aryan-25 aryan-25 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Motivation

The required TLS contexts/configurations required to create the server channels are only constructed when NIOHTTPServer.serve is invoked. The problem with this is that these constructions can throw an error if the provided configuration was invalid.

We should not allow an invalid NIOHTTPServerConfiguration instance to be created in the first place: validation should occur during the initialization of NIOHTTPServerConfiguration.

Modifications

  • NIOHTTPServerConfiguration's initializer now validates that supportedHTTPVersions and transportSecurity are compatible with each other, and stores the necessary TLS objects.
    • NIOHTTPServer.serve .serve can therefore use these properties directly and not invoke any throwing method relating to configuration.
  • Some small general refactors to improve readability.

Result

Validation of NIOHTTPServerConfiguration now happens at initialization rather than during NIOHTTPServer.serve.

@aryan-25
aryan-25 requested a review from gjcairo August 3, 2026 16:01
@aryan-25 aryan-25 added the ⚠️ semver/major Breaks existing public API. label Aug 3, 2026
@available(anyAppleOS 26.0, *)
@Test(
"All X.509 credential sources produce a valid configuration",
arguments: [TestX509CredentialSource]([.inMemory, .reloading, .pemFile, .derFile, .pemBytes, .derBytes])

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.

Nit, but for consistency with other tests/files/packages:

Suggested change
arguments: [TestX509CredentialSource]([.inMemory, .reloading, .pemFile, .derFile, .pemBytes, .derBytes])
arguments: [TestX509CredentialSource.inMemory, .reloading, .pemFile, .derFile, .pemBytes, .derBytes]

This has to change in a few places.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commits 8da01f8 and bd7757d.

func nonExistentX509FilePathRejected(
credentials: NIOHTTPServerConfiguration.TransportSecurity.X509Credentials
) throws {
#expect(throws: Error.self) {

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.

We can't assert a more specific error type or message?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Different errors are returned in both cases by swift-nio-ssl. The PEM case throws a NIOSSLError.failedToLoadCertificate error and the DER case throws a NIOCore.IOError.

I felt that explicitly catching these errors clutters the test case without adding much benefit.

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.

I only worry that expecting any error may mean that even though this behaviour changes in the future (e.g. a new error we should be handling different starts being thrown), the test will still pass.

}

@available(anyAppleOS 26.0, *)
enum TestX509CredentialSource: Sendable {

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.

We don't have a type like this in the actual configuration types? Feels a bit weird to have this parallel type in the tests instead of an extension on that type similarly to what we're doing below with NIOHTTPServerConfiguration.TransportSecurity.RawPublicKeyCredentials.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The actual configuration types don't allow us to express the cases (e.g. inMemory, reloading, etc.) without providing associated values.

These parallel types allow us to parameterise over all cases quite easily. There is only one supported format for RPK credentials (DER files), which is why I didn't bother for that case.

#endif // HTTP3

@available(anyAppleOS 26.0, *)
enum MTLSTrustSource: Sendable {

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 question here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Answered here.

Comment on lines +382 to +388
extension NIOHTTPServerConfiguration.HTTPVersion {
static let http2 = Self.http2(config: .defaults)

#if HTTP3
static let http3 = Self.http3(config: .defaults)
#endif
}

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.

Perhaps we should default config to .defaults in the init instead of having this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit 6605bc2.


/// Network binding configuration specifying all addresses where the server should listen.
public var bindTargets: [BindTarget]
public let bindTargets: [BindTarget]

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.

We generally keep properties in config structs as vars so they can be modified. This is common across all of our packages AFAIK. Did you make this change for a specific reason?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes. If any of these three properties (bindTargets, transportSecurity, supportedHTTPVersions) are modified after initialization, then the two new contexts we store may become invalid.

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.

We can add a willSet to these properties and do the validation/update of the contexts there.

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.

We discussed offline and because we can't have throwing setters we will probably just precondition failure.
Thinking more about it though, because this wouldn't fail on release builds, I think it's better to avoid having the ValidatedX types and instead just do validations in the init/setters, without encapsulating in separate types.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've now updated the PR with this approach.

Comment thread Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration+Validation.swift Outdated
///
/// - Parameter config: The configuration to use for HTTP/2.
public static func http2(config: HTTP2) -> Self {
public static func http2(config: HTTP2 = .defaults) -> Self {

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.

Nit, but I think additionally having a static var for h2 and h3 that defaults the config to .defaults may make the API slightly nicer since we can avoid the empty brackets (.http2 instead of .http2())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in commit b32144d.

@aryan-25
aryan-25 requested a review from gjcairo August 6, 2026 16:53
@FranzBusch

Copy link
Copy Markdown
Contributor

This is interesting. A potential side observation: This seems to boil down to differences in support transport security for H1/2/3. It would be great if this is a compile time guarantee rather than a runtime enforced. Have we considered making the transport security structs HTTP version specific?

@aryan-25

Copy link
Copy Markdown
Collaborator Author

This seems to boil down to differences in support transport security for H1/2/3

Yes. Although it is worth nothing that most of the differences today just arise from a mismatch in the credential sources supported by swift-nio-ssl and swift-nio-quic. From swift-nio-quic, we need support for (1) byte-array, DER-file, and in-memory swift-certificates sources, (2) certificate reloading, and (3) mTLS (with support for a custom verification callback). From swift-nio-ssl, we need support for Raw Public Key sources.

Once there is parity, the only difference that remains is transportSecurity == .plaintext being possible over HTTP/1.1 (and HTTP/2 once we add support for h2c), but not over HTTP/3.

It feels odd to have a transportSecurity type for HTTP/3 and a very similar one for HTTP/1.1 / HTTP/2 that additionally just allows plaintext to be expressed. Having a transportSecurity per supported HTTP version may not also be the right shape because when supportedHTTPVersions == [.http1_1, .http2], peers may be intending to use either HTTP/1.1 or HTTP/2, but we only find out through the TLS handshake (ALPN), which means we can only support a single set of TLS credentials.

/// - `transportSecurity` can only be set to `.plaintext` when `supportedHTTPVersions == [.http1_1]`.
public var transportSecurity: TransportSecurity {
didSet {
try! self.validateTransportConfiguration()

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.

I don't think we should try! here. I think it's okay to precondition failure because we won't crash at runtime on release builds, but with this we could. We should throw in throwing contexts and precondition failure where we can't throw.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Note that preconditionFailure will also crash in release builds (assuming standard release build configs). It will only not crash when the package is built with -Ounchecked, which is a non-standard release build config: https://developer.apple.com/documentation/swift/preconditionfailure(_:file:line:)#discussion.

But I've removed the try! and replaced it with preconditionFailure for consistency in commit 5c29ae1.

@aryan-25
aryan-25 requested a review from gjcairo August 12, 2026 08:31
@FranzBusch

Copy link
Copy Markdown
Contributor

Yes. Although it is worth nothing that most of the differences today just arise from a mismatch in the credential sources supported by swift-nio-ssl and swift-nio-quic. From swift-nio-quic, we need support for (1) byte-array, DER-file, and in-memory swift-certificates sources, (2) certificate reloading, and (3) mTLS (with support for a custom verification callback). From swift-nio-ssl, we need support for Raw Public Key sources.

Once there is parity, the only difference that remains is transportSecurity == .plaintext being possible over HTTP/1.1 (and HTTP/2 once we add support for h2c), but not over HTTP/3.

It feels odd to have a transportSecurity type for HTTP/3 and a very similar one for HTTP/1.1 / HTTP/2 that additionally just allows plaintext to be expressed. Having a transportSecurity per supported HTTP version may not also be the right shape because when supportedHTTPVersions == [.http1_1, .http2], peers may be intending to use either HTTP/1.1 or HTTP/2, but we only find out through the TLS handshake (ALPN), which means we can only support a single set of TLS credentials.

I understand where the current differences come from. Just to explain this a bit more. What if a user wants to bootstrap a server that serves H/1 over plaintext and H2 over TLS? This is currently not configurable right? This might be fine just trying to provide another argument why separate H1/2/3/ properties might be beneficial. I agree though that they should use the same type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ semver/major Breaks existing public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants