From 81351c00f69dd0ae0622a8d0dd27e549a48d40a8 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Tue, 14 Jul 2026 19:01:13 +0100 Subject: [PATCH 1/6] Introduce HTTP/3 configuration --- .github/workflows/apibreakage.yml | 35 +++ .github/workflows/main.yml | 5 +- .github/workflows/pull_request.yml | 7 +- Package.swift | 8 +- README.md | 20 ++ .../HTTP3/HTTP3+ConnectionSettings.swift | 60 ++++ .../HTTP3/HTTP3+ProtocolConfiguration.swift | 40 +++ .../HTTP3/HTTP3+QUICConfiguration.swift | 286 ++++++++++++++++++ .../NIOHTTPServerConfiguration+HTTP3.swift | 61 ++++ .../NIOHTTPServerConfigurationError.swift | 11 +- .../HTTP3ConfigurationTests.swift | 104 +++++++ 11 files changed, 633 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/apibreakage.yml create mode 100644 Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift create mode 100644 Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift create mode 100644 Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift create mode 100644 Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift create mode 100644 Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift diff --git a/.github/workflows/apibreakage.yml b/.github/workflows/apibreakage.yml new file mode 100644 index 0000000..95382e5 --- /dev/null +++ b/.github/workflows/apibreakage.yml @@ -0,0 +1,35 @@ +name: api breakage checks + +permissions: + contents: read + +on: + workflow_call: + +jobs: + api-breakage-check: + name: API breakage check + runs-on: ubuntu-latest + container: + image: swiftlang/swift:nightly-6.4.x-noble + timeout-minutes: 40 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + persist-credentials: false + fetch-depth: 0 # Fetching tags requires fetch-depth: 0 (https://github.com/actions/checkout/issues/1471) + - name: Mark the workspace as safe + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Run API breakage check + shell: bash + # While depending on swift-crypto beta releases, an environment variable is required + # to allow swift-certificates to accept the beta versions. + env: + SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA: 1 + run: | + git fetch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} ${GITHUB_BASE_REF}:pull-base-ref + BASELINE_REF='pull-base-ref' + echo "Using baseline: $BASELINE_REF" + swift package diagnose-api-breaking-changes "$BASELINE_REF" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70b331a..b8c06f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,14 +19,16 @@ jobs: linux_6_1_enabled: false linux_6_2_enabled: false linux_6_3_enabled: false - # linux_6_3_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_next_enabled: true linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' static-sdk: name: Static SDK uses: apple/swift-nio/.github/workflows/static_sdk.yml@main + with: + env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' release-builds: name: Release builds @@ -38,3 +40,4 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: true + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 88fcd38..c19c21e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,7 +12,7 @@ jobs: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11 with: - api_breakage_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" + api_breakage_check_enabled: false format_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" license_header_check_project_name: "Swift HTTP Server" @@ -29,6 +29,7 @@ jobs: linux_nightly_next_enabled: true linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' cxx-interop: name: Cxx interop @@ -40,10 +41,13 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: true + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' static-sdk: name: Static SDK uses: apple/swift-nio/.github/workflows/static_sdk.yml@main + with: + env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' release-builds: name: Release builds @@ -55,3 +59,4 @@ jobs: linux_6_2_enabled: false linux_6_3_enabled: false linux_nightly_next_enabled: false # should be disabled until next is 6.4 + linux_env_vars: '{"SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA":"1"}' diff --git a/Package.swift b/Package.swift index 11a5bd2..d7ace5f 100644 --- a/Package.swift +++ b/Package.swift @@ -53,9 +53,12 @@ let package = Package( .upToNextMinor(from: "0.2.0") ), .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.4.1"), - .package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.1"), + .package(url: "https://github.com/apple/swift-certificates.git", from: "1.19.3"), .package(url: "https://github.com/apple/swift-log.git", from: "1.14.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"), + .package(url: "https://github.com/apple/swift-nio-quic.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/apple/swift-nio-quic-helpers.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/apple/swift-nio-http3.git", .upToNextMinor(from: "0.1.0")), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.37.0"), .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.34.1"), .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.44.0"), @@ -110,6 +113,9 @@ let package = Package( condition: .when(traits: ["Configuration"]) ), .product(name: "NIOExtras", package: "swift-nio-extras"), + .product(name: "NIOQUIC", package: "swift-nio-quic"), + .product(name: "NIOQUICHelpers", package: "swift-nio-quic-helpers"), + .product(name: "NIOHTTP3", package: "swift-nio-http3"), .product(name: "HTTPAPIs", package: "swift-http-api-proposal"), ], swiftSettings: extraSettings diff --git a/README.md b/README.md index c77a295..5b82c0a 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,23 @@ This package offers additional integrations you can enable using Available traits: - **`Configuration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration` `ConfigProvider`. + +## HTTP/3 support + +Packages in the dependency tree depend on a beta release of swift-crypto. +Set the environment variable +`SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA` to allow swift-certificates +(in the dependency tree) to adopt swift-crypto beta releases as well. + +``` +SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 swift build +``` + +To run all unit tests, run + +``` +SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 swift test +``` + +Use `SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1 xed Package.swift` to open +the project in Xcode with the environment variable set. diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift new file mode 100644 index 0000000..84c608f --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HTTP3 + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.HTTP3 { + /// HTTP/3 connection settings sent to the peer during connection establishment. + public struct ConnectionSettings: Sendable, Hashable { + /// The maximum capacity of the QPACK dynamic table. The default value is 0. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.2.1. Corresponds to + /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY`. + public var qpackMaximumTableCapacity: UInt64 + + /// The maximum number of streams which may be blocked on QPACK at any one time. The default value is 0. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.4.1. Corresponds to + /// `SETTINGS_QPACK_BLOCKED_STREAMS`. + public var qpackBlockedStreams: UInt64 + + /// The maximum size of a field section. The default value is `nil`, which is interpreted as there being no + /// field section size limit. + /// + /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1. Corresponds to + /// `SETTINGS_MAX_FIELD_SECTION_SIZE`. + public var maximumFieldSectionSize: UInt64? + + /// The default configuration. + public static var defaults: Self { + Self( + qpackMaximumTableCapacity: 0, + qpackBlockedStreams: 0, + maximumFieldSectionSize: nil + ) + } + } +} + +@available(anyAppleOS 26.0, *) +extension HTTP3.HTTP3Settings { + init(_ configuration: NIOHTTPServerConfiguration.HTTP3.ConnectionSettings) { + self.init( + qpackMaximumTableCapacity: configuration.qpackMaximumTableCapacity, + qpackBlockedStreams: configuration.qpackBlockedStreams, + maximumFieldSectionSize: configuration.maximumFieldSectionSize + ) + } +} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift new file mode 100644 index 0000000..70d27d2 --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOHTTP3 + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.HTTP3 { + /// HTTP/3 protocol-level configuration. + public struct ProtocolConfiguration: Sendable, Hashable { + /// If true, Huffman encoding will be used where applicable, e.g. for header field sections. + /// + /// - Note: Huffman encoding will not be used if it would result in a larger payload than not using it, even if + /// this property is true. + public var preferHuffmanEncoding: Bool + + /// The default HTTP/3 protocol configuration. + public static var defaults: Self { + Self(preferHuffmanEncoding: true) + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOHTTP3.HTTP3ServerConfiguration { + init(_ configuration: NIOHTTPServerConfiguration.HTTP3.ProtocolConfiguration) { + self = .defaults + self.preferHuffmanEncoding = configuration.preferHuffmanEncoding + } +} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift new file mode 100644 index 0000000..4325e14 --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift @@ -0,0 +1,286 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOQUIC + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration.HTTP3 { + /// QUIC transport configuration for an HTTP/3 server. + public struct QUICConfiguration: Sendable, Hashable { + /// Configuration for writing qlog files, which capture QUIC and HTTP/3 events for debugging and analysis. + /// + /// - SeeAlso: https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-13.html + public struct QLogConfiguration: Sendable, Hashable { + /// The directory to where the qlog files are written to. + public var path: String + + /// The title to use when logging. + public var topic: String + + /// The description to use when logging. + public var description: String + + /// Creates a qlog configuration with the given directory, topic, and description. + /// + /// - Parameters: + /// - path: The directory to write qlog files to. + /// - topic: The title to use when logging. + /// - description: The description to use when logging. + public init(path: String, topic: String, description: String) { + self.path = path + self.topic = topic + self.description = description + } + } + + /// The TLS 1.3 key exchange named group. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.7 and + /// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 + public struct KeyExchangeGroup: Sendable, Hashable { + enum Backing: UInt16 { + case secp256 = 0x0017 + case secp384 = 0x0018 + case x25519 = 0x001D + case x25519MLKEM768 = 0x11EC + } + + let backing: Backing + + /// The NIST P-256 elliptic curve. + public static var secp256: Self { + .init(backing: .secp256) + } + + /// The NIST P-384 elliptic curve. + public static var secp384: Self { + .init(backing: .secp384) + } + + /// The X25519 elliptic curve (Curve25519). + public static var x25519: Self { + .init(backing: .x25519) + } + + /// A post-quantum hybrid group that combines X25519 with the ML-KEM-768 key encapsulation mechanism. + public static var x25519MLKEM768: Self { + .init(backing: .x25519MLKEM768) + } + } + + /// The server's hostname for the TLS handshake. + /// + /// - Important: SwiftTLS currently just ignores the server name sent in the ClientHello. See + /// https://github.com/apple/swift-nio-quic/issues/4. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc6066#section-3 + public var serverName: String + + /// The named group to use for the TLS 1.3 key exchange. + /// + /// - Note: The default value is ``KeyExchangeGroup/x25519``. + public var keyExchangeGroup: KeyExchangeGroup + + /// The idle timeout advertised to the client. A connection may time out sooner than this value if the client + /// advertises a shorter idle timeout. + /// + /// - Important: The effective idle timeout enforced on a connection is the minimum of both endpoints' + /// advertised values. + /// + /// - Note: The default value is 30 seconds. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.4.1 and + /// https://datatracker.ietf.org/doc/html/rfc9000#name-idle-timeout + public var maxIdleTimeout: Duration + + /// The initial value for the maximum amount of data (in bytes) that can be sent on the connection. + /// + /// - Note: The default value is 1 MiB. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.14.1 + public var initialMaxData: Int + + /// The initial flow control limit for locally initiated bidirectional streams. + /// + /// - Note: The default value is 1 MiB. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.16.1 + public var initialMaxStreamDataBidirectionalLocal: Int + + /// The initial flow control limit for client-initiated bidirectional streams. + /// + /// - Note: The default value is 1 MiB. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.18.1 + public var initialMaxStreamDataBidirectionalRemote: Int + + /// The initial flow control limit for unidirectional streams. + /// + /// - Note: The default value is 1 MiB. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.20.1 + public var initialMaxStreamDataUnidirectional: Int + + /// The initial maximum number of bidirectional streams the server is permitted to initiate. + /// + /// - Note: The default value is 100 streams. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.22.1 + public var initialMaxStreamsBidirectional: Int + + /// The initial maximum number of unidirectional streams the server is permitted to initiate. + /// + /// - Note: The default value is 100 streams. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.24.1 + public var initialMaxStreamsUnidirectional: Int + + /// The interval at which the server sends keep-alive PING frames. + /// + /// Each PING restarts both endpoints' idle timers. The server's idle timer is restarted when the PING is sent, + /// and the peer's idle timer is restarted when the PING is received. This prevents the connection from being + /// closed by ``maxIdleTimeout``. + /// + /// - Important: For keep-alive pings to be effective, the interval must be shorter than the negotiated idle + /// timeout. + /// + /// - Note: Defaults to `nil` (no keep-alive PINGs are sent). + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-10.1.2 + public var keepAliveInterval: Duration? + + /// Whether the server sends a Retry packet before accepting a new connection. + /// + /// - Note: Defaults to `false`. + /// + /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-8.1.2 + public var sendRetry: Bool + + /// The path to a file where TLS session keys are logged in NSS Key Log format. + /// + /// When set, tools such as Wireshark can use this file to decrypt captured QUIC traffic. + public var keyLogPath: String? + + /// Optional qlog configuration. + /// + /// When set, QUIC and HTTP/3 events are written to qlog files in the specified directory, which is useful for + /// debugging and analysis. + public var qLogConfiguration: QLogConfiguration? + + /// The default QUIC transport configuration. + public static var defaults: Self { + Self( + // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. This default configuration + // just sets `serverName` to an empty string. See https://github.com/apple/swift-nio-quic/issues/4. + serverName: "", + keyExchangeGroup: .x25519, + maxIdleTimeout: .seconds(30), + initialMaxData: 1024 * 1024, + initialMaxStreamDataBidirectionalLocal: 1024 * 1024, + initialMaxStreamDataBidirectionalRemote: 1024 * 1024, + initialMaxStreamDataUnidirectional: 1024 * 1024, + initialMaxStreamsBidirectional: 100, + initialMaxStreamsUnidirectional: 100, + keepAliveInterval: nil, + sendRetry: false, + keyLogPath: nil, + qLogConfiguration: nil + ) + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.QUICConfiguration.QLogConfiguration { + fileprivate init(_ configuration: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.QLogConfiguration) { + self.init( + path: configuration.path, + topic: configuration.topic, + description: configuration.description + ) + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.KeyExchangeGroup { + fileprivate init(_ configuration: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.KeyExchangeGroup) { + switch configuration.backing { + case .secp256: + self = .secp256 + + case .secp384: + self = .secp384 + + case .x25519: + self = .x25519 + + case .x25519MLKEM768: + self = .x25519MLKEM768 + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.AuthenticationConfiguration { + init(_ transportSecurity: NIOHTTPServerConfiguration.TransportSecurity) throws { + switch transportSecurity.backing { + case .plaintext: + throw NIOHTTPServerConfigurationError.incompatibleTransportSecurity + + case .mTLS: + throw NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3 + + case .tls(let credentials): + switch credentials.backing { + case .inMemory, .reloading: + throw NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 + + case .pemFile(let certificateChainPath, let privateKeyPath): + self = .x509Certificates( + certificateChainFilePath: certificateChainPath, + privateKeyFilePath: privateKeyPath + ) + } + } + } +} + +@available(anyAppleOS 26.0, *) +extension NIOQUIC.QUICConfiguration { + /// Creates a `NIOQUIC.QUICConfiguration` from a `NIOHTTPServerConfiguration.HTTP3.QUICConfiguration` instance. + init( + _ config: NIOHTTPServerConfiguration.HTTP3.QUICConfiguration, + authenticationConfiguration: NIOQUIC.AuthenticationConfiguration + ) { + self = .server( + // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. See + // https://github.com/apple/swift-nio-quic/issues/4. + serverName: config.serverName, + authenticationConfiguration: authenticationConfiguration, + keyExchangeGroup: .init(config.keyExchangeGroup), + applicationProtocols: ["h3"], + maxIdleTimeout: config.maxIdleTimeout, + initialMaxData: config.initialMaxData, + initialMaxStreamDataBidiLocal: config.initialMaxStreamDataBidirectionalLocal, + initialMaxStreamDataBidiRemote: config.initialMaxStreamDataBidirectionalRemote, + initialMaxStreamDataUni: config.initialMaxStreamDataUnidirectional, + initialMaxStreamsBidi: config.initialMaxStreamsBidirectional, + initialMaxStreamsUni: config.initialMaxStreamsUnidirectional, + keepAliveInterval: config.keepAliveInterval, + sendRetry: config.sendRetry, + keyLogPath: config.keyLogPath, + qLogConfiguration: config.qLogConfiguration.map { .init($0) } + ) + } +} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift new file mode 100644 index 0000000..625ab37 --- /dev/null +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +@available(anyAppleOS 26.0, *) +extension NIOHTTPServerConfiguration { + /// Configuration for HTTP/3. + public struct HTTP3: Sendable, Hashable { + /// QUIC transport configuration. + public var quicConfiguration: QUICConfiguration = .defaults + + /// HTTP/3 protocol-level configuration. + public var protocolConfiguration: ProtocolConfiguration = .defaults + + /// HTTP/3 connection settings exchanged with the client during connection establishment. + public var connectionSettings: ConnectionSettings = .defaults + + /// Creates an HTTP/3 configuration with the given HTTP/3 protocol configuration, connection settings, and QUIC + /// transport configuration. + /// + /// - Parameters: + /// - quicConfiguration: QUIC transport parameters. + /// - protocolConfiguration: Settings that control HTTP/3 protocol behaviour. + /// - connectionSettings: HTTP/3 connection-level settings exchanged with the client. + public init( + quicConfiguration: QUICConfiguration, + protocolConfiguration: ProtocolConfiguration, + connectionSettings: ConnectionSettings, + ) { + self.protocolConfiguration = protocolConfiguration + self.connectionSettings = connectionSettings + self.quicConfiguration = quicConfiguration + } + + /// The default configuration. + public static var defaults: Self { + Self( + quicConfiguration: .defaults, + protocolConfiguration: .defaults, + connectionSettings: .defaults, + ) + } + + // The fallback connection RTT to use if there is an error obtaining the RTT estimate channel option. + static var fallbackConnectionRTT: TimeAmount { + .milliseconds(100) + } + } +} diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift index 7f56f9c..19ba873 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfigurationError.swift @@ -17,6 +17,9 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { case noSupportedHTTPVersionsSpecified case incompatibleTransportSecurity case noBindTargetsSpecified + case onlyPEMFileCredentialsCurrentlySupportedOverHTTP3 + // swift-nio-quic doesn't currently support mTLS. See https://github.com/apple/swift-nio-quic/issues/5. + case mTLSNotCurrentlySupportedOverHTTP3 var description: String { switch self { @@ -24,10 +27,16 @@ enum NIOHTTPServerConfigurationError: Error, CustomStringConvertible { "Invalid configuration: at least one supported HTTP version must be specified." case .incompatibleTransportSecurity: - "Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2." + "Invalid configuration: only HTTP/1.1 can be served over plaintext. `transportSecurity` must be set to (m)TLS for serving HTTP/2 or HTTP/3." case .noBindTargetsSpecified: "Invalid configuration: at least one bind target must be specified." + + case .onlyPEMFileCredentialsCurrentlySupportedOverHTTP3: + "Invalid configuration: only PEM-file X.509 credentials are supported over HTTP/3. In-memory or reloading credential sources are not currently supported." + + case .mTLSNotCurrentlySupportedOverHTTP3: + "Invalid configuration: mTLS is not currently supported over HTTP/3." } } } diff --git a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift new file mode 100644 index 0000000..d6cfb27 --- /dev/null +++ b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP Server open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOQUIC +import Testing + +@testable import NIOHTTPServer + +@Suite +struct HTTP3ConfigurationTests { + @Test("HTTP/3 default configuration uses the defaults of the three sub-components") + @available(anyAppleOS 26.0, *) + func http3DefaultConfiguration() { + let config = NIOHTTPServerConfiguration.HTTP3.defaults + #expect(config.quicConfiguration == .defaults) + #expect(config.protocolConfiguration == .defaults) + #expect(config.connectionSettings == .defaults) + } + + @Test("HTTP/3 configuration with custom values") + @available(anyAppleOS 26.0, *) + func http3ConfigurationCustomValues() { + var protocolConfiguration = NIOHTTPServerConfiguration.HTTP3.ProtocolConfiguration.defaults + protocolConfiguration.preferHuffmanEncoding = false + + var connectionSettings = NIOHTTPServerConfiguration.HTTP3.ConnectionSettings.defaults + connectionSettings.qpackMaximumTableCapacity = 4096 + connectionSettings.qpackBlockedStreams = 16 + connectionSettings.maximumFieldSectionSize = 8192 + + var quic = NIOHTTPServerConfiguration.HTTP3.QUICConfiguration.defaults + quic.keepAliveInterval = .seconds(10) + quic.sendRetry = true + + let config = NIOHTTPServerConfiguration.HTTP3( + quicConfiguration: quic, + protocolConfiguration: protocolConfiguration, + connectionSettings: connectionSettings + ) + + #expect(config.protocolConfiguration.preferHuffmanEncoding == false) + #expect(config.connectionSettings.qpackMaximumTableCapacity == 4096) + #expect(config.connectionSettings.qpackBlockedStreams == 16) + #expect(config.connectionSettings.maximumFieldSectionSize == 8192) + #expect(config.quicConfiguration.keepAliveInterval == .seconds(10)) + #expect(config.quicConfiguration.sendRetry == true) + } + + @Suite + struct AuthenticationConfigurationTests { + @Test("Plaintext transport security is rejected") + @available(anyAppleOS 26.0, *) + func plaintextRejected() { + #expect(throws: NIOHTTPServerConfigurationError.incompatibleTransportSecurity) { + _ = try NIOQUIC.AuthenticationConfiguration(.plaintext) + } + } + + @Test("mTLS transport security is rejected") + @available(anyAppleOS 26.0, *) + func mTLSRejected() { + #expect(throws: NIOHTTPServerConfigurationError.mTLSNotCurrentlySupportedOverHTTP3) { + _ = try NIOQUIC.AuthenticationConfiguration( + .mTLS( + credentials: .pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem"), + trustConfiguration: .pemFile(path: "/roots.pem") + ) + ) + } + } + + @Test("In-memory TLS credentials are rejected") + @available(anyAppleOS 26.0, *) + func inMemoryCredentialsRejected() throws { + let chain = try TestCA.makeSelfSignedChain() + #expect(throws: NIOHTTPServerConfigurationError.onlyPEMFileCredentialsCurrentlySupportedOverHTTP3) { + _ = try NIOQUIC.AuthenticationConfiguration( + .tls(credentials: .inMemory(certificateChain: chain.chain, privateKey: chain.privateKey)) + ) + } + } + + @Test("PEM-file TLS credentials are accepted") + @available(anyAppleOS 26.0, *) + func pemFileCredentialsAccepted() { + #expect(throws: Never.self) { + _ = try NIOQUIC.AuthenticationConfiguration( + .tls(credentials: .pemFile(certificateChainPath: "/cert.pem", privateKeyPath: "/key.pem")) + ) + } + } + } +} From 57f822903feb6d3ffe83366fef31ab48ad5e8db5 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 15 Jul 2026 17:15:57 +0100 Subject: [PATCH 2/6] Fix docs --- .../HTTP3/HTTP3+QUICConfiguration.swift | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift index 4325e14..933073d 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift @@ -88,8 +88,6 @@ extension NIOHTTPServerConfiguration.HTTP3 { public var serverName: String /// The named group to use for the TLS 1.3 key exchange. - /// - /// - Note: The default value is ``KeyExchangeGroup/x25519``. public var keyExchangeGroup: KeyExchangeGroup /// The idle timeout advertised to the client. A connection may time out sooner than this value if the client @@ -98,51 +96,37 @@ extension NIOHTTPServerConfiguration.HTTP3 { /// - Important: The effective idle timeout enforced on a connection is the minimum of both endpoints' /// advertised values. /// - /// - Note: The default value is 30 seconds. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.4.1 and /// https://datatracker.ietf.org/doc/html/rfc9000#name-idle-timeout public var maxIdleTimeout: Duration /// The initial value for the maximum amount of data (in bytes) that can be sent on the connection. /// - /// - Note: The default value is 1 MiB. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.14.1 public var initialMaxData: Int /// The initial flow control limit for locally initiated bidirectional streams. /// - /// - Note: The default value is 1 MiB. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.16.1 public var initialMaxStreamDataBidirectionalLocal: Int /// The initial flow control limit for client-initiated bidirectional streams. /// - /// - Note: The default value is 1 MiB. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.18.1 public var initialMaxStreamDataBidirectionalRemote: Int /// The initial flow control limit for unidirectional streams. /// - /// - Note: The default value is 1 MiB. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.20.1 public var initialMaxStreamDataUnidirectional: Int /// The initial maximum number of bidirectional streams the server is permitted to initiate. /// - /// - Note: The default value is 100 streams. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.22.1 public var initialMaxStreamsBidirectional: Int /// The initial maximum number of unidirectional streams the server is permitted to initiate. /// - /// - Note: The default value is 100 streams. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-18.2-4.24.1 public var initialMaxStreamsUnidirectional: Int @@ -155,15 +139,11 @@ extension NIOHTTPServerConfiguration.HTTP3 { /// - Important: For keep-alive pings to be effective, the interval must be shorter than the negotiated idle /// timeout. /// - /// - Note: Defaults to `nil` (no keep-alive PINGs are sent). - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-10.1.2 public var keepAliveInterval: Duration? /// Whether the server sends a Retry packet before accepting a new connection. /// - /// - Note: Defaults to `false`. - /// /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc9000#section-8.1.2 public var sendRetry: Bool @@ -179,6 +159,20 @@ extension NIOHTTPServerConfiguration.HTTP3 { public var qLogConfiguration: QLogConfiguration? /// The default QUIC transport configuration. + /// + /// Uses the following default values: + /// - `keyExchangeGroup`: ``KeyExchangeGroup/x25519`` + /// - `maxIdleTimeout`: 30 seconds + /// - `initialMaxData`: 1 MiB + /// - `initialMaxStreamDataBidirectionalLocal`: 1 MiB + /// - `initialMaxStreamDataBidirectionalRemote`: 1 MiB + /// - `initialMaxStreamDataUnidirectional`: 1 MiB + /// - `initialMaxStreamsBidirectional`: 100 streams + /// - `initialMaxStreamsUnidirectional`: 100 streams + /// - `keepAliveInterval`: `nil` (no keep-alive PINGs are sent) + /// - `sendRetry`: `false` + /// - `keyLogPath`: `nil` (TLS session keys are not logged) + /// - `qLogConfiguration`: `nil` (qlog is not enabled) public static var defaults: Self { Self( // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. This default configuration From 555877dfcac0631566f4d5547f5b22b7551af9d9 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 15 Jul 2026 17:41:32 +0100 Subject: [PATCH 3/6] Make docs consistent --- .../HTTP3/HTTP3+ConnectionSettings.swift | 14 +++++++---- .../HTTP3/HTTP3+ProtocolConfiguration.swift | 2 ++ .../HTTP3/HTTP3+QUICConfiguration.swift | 24 +++++++++---------- .../NIOHTTPServerConfiguration+HTTP3.swift | 7 +++++- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift index 84c608f..6949816 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ConnectionSettings.swift @@ -18,26 +18,30 @@ import HTTP3 extension NIOHTTPServerConfiguration.HTTP3 { /// HTTP/3 connection settings sent to the peer during connection establishment. public struct ConnectionSettings: Sendable, Hashable { - /// The maximum capacity of the QPACK dynamic table. The default value is 0. + /// The maximum capacity of the QPACK dynamic table. /// /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.2.1. Corresponds to /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY`. public var qpackMaximumTableCapacity: UInt64 - /// The maximum number of streams which may be blocked on QPACK at any one time. The default value is 0. + /// The maximum number of streams which may be blocked on QPACK at any one time. /// /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9204.html#section-5-2.4.1. Corresponds to /// `SETTINGS_QPACK_BLOCKED_STREAMS`. public var qpackBlockedStreams: UInt64 - /// The maximum size of a field section. The default value is `nil`, which is interpreted as there being no - /// field section size limit. + /// The maximum size of a field section. /// /// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1. Corresponds to /// `SETTINGS_MAX_FIELD_SECTION_SIZE`. public var maximumFieldSectionSize: UInt64? - /// The default configuration. + /// The default HTTP/3 connection settings configuration. + /// + /// Uses the following default values: + /// - `qpackMaximumTableCapacity`: 0. + /// - `qpackBlockedStreams`: 0. + /// - `maximumFieldSectionSize`: `nil` (no field section size limit). public static var defaults: Self { Self( qpackMaximumTableCapacity: 0, diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift index 70d27d2..d01206c 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift @@ -25,6 +25,8 @@ extension NIOHTTPServerConfiguration.HTTP3 { public var preferHuffmanEncoding: Bool /// The default HTTP/3 protocol configuration. + /// + /// Sets `preferHuffmanEncoding` to `true`. public static var defaults: Self { Self(preferHuffmanEncoding: true) } diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift index 933073d..2a44a8f 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+QUICConfiguration.swift @@ -161,18 +161,18 @@ extension NIOHTTPServerConfiguration.HTTP3 { /// The default QUIC transport configuration. /// /// Uses the following default values: - /// - `keyExchangeGroup`: ``KeyExchangeGroup/x25519`` - /// - `maxIdleTimeout`: 30 seconds - /// - `initialMaxData`: 1 MiB - /// - `initialMaxStreamDataBidirectionalLocal`: 1 MiB - /// - `initialMaxStreamDataBidirectionalRemote`: 1 MiB - /// - `initialMaxStreamDataUnidirectional`: 1 MiB - /// - `initialMaxStreamsBidirectional`: 100 streams - /// - `initialMaxStreamsUnidirectional`: 100 streams - /// - `keepAliveInterval`: `nil` (no keep-alive PINGs are sent) - /// - `sendRetry`: `false` - /// - `keyLogPath`: `nil` (TLS session keys are not logged) - /// - `qLogConfiguration`: `nil` (qlog is not enabled) + /// - `keyExchangeGroup`: ``KeyExchangeGroup/x25519``. + /// - `maxIdleTimeout`: 30 seconds. + /// - `initialMaxData`: 1 MiB. + /// - `initialMaxStreamDataBidirectionalLocal`: 1 MiB. + /// - `initialMaxStreamDataBidirectionalRemote`: 1 MiB. + /// - `initialMaxStreamDataUnidirectional`: 1 MiB. + /// - `initialMaxStreamsBidirectional`: 100 streams. + /// - `initialMaxStreamsUnidirectional`: 100 streams. + /// - `keepAliveInterval`: `nil` (no keep-alive PINGs are sent). + /// - `sendRetry`: `false`. + /// - `keyLogPath`: `nil` (TLS session keys are not logged). + /// - `qLogConfiguration`: `nil` (qlog is not enabled). public static var defaults: Self { Self( // SwiftTLS currently just ignores the `serverName` sent in the ClientHello. This default configuration diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift index 625ab37..c150762 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift @@ -44,7 +44,12 @@ extension NIOHTTPServerConfiguration { self.quicConfiguration = quicConfiguration } - /// The default configuration. + /// The default HTTP/3 configuration. + /// + /// Uses the default configurations of the sub-components: + /// - `quicConfiguration`: ``QUICConfiguration/defaults``. + /// - `protocolConfiguration`: ``ProtocolConfiguration/defaults``. + /// - `connectionSettings`: ``ConnectionSettings/defaults``. public static var defaults: Self { Self( quicConfiguration: .defaults, From ffdfded2d99d24d41a532e39b471592b6cca957c Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 16 Jul 2026 13:41:02 +0100 Subject: [PATCH 4/6] Remove nested `ProtocolConfiguration` type; inline it in the HTTP/3 configuration. --- .../HTTP3/HTTP3+ProtocolConfiguration.swift | 42 ------------------- .../NIOHTTPServerConfiguration+HTTP3.swift | 33 ++++++++++----- .../HTTP3ConfigurationTests.swift | 10 ++--- 3 files changed, 25 insertions(+), 60 deletions(-) delete mode 100644 Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift deleted file mode 100644 index d01206c..0000000 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/HTTP3+ProtocolConfiguration.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift HTTP Server open source project -// -// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOHTTP3 - -@available(anyAppleOS 26.0, *) -extension NIOHTTPServerConfiguration.HTTP3 { - /// HTTP/3 protocol-level configuration. - public struct ProtocolConfiguration: Sendable, Hashable { - /// If true, Huffman encoding will be used where applicable, e.g. for header field sections. - /// - /// - Note: Huffman encoding will not be used if it would result in a larger payload than not using it, even if - /// this property is true. - public var preferHuffmanEncoding: Bool - - /// The default HTTP/3 protocol configuration. - /// - /// Sets `preferHuffmanEncoding` to `true`. - public static var defaults: Self { - Self(preferHuffmanEncoding: true) - } - } -} - -@available(anyAppleOS 26.0, *) -extension NIOHTTP3.HTTP3ServerConfiguration { - init(_ configuration: NIOHTTPServerConfiguration.HTTP3.ProtocolConfiguration) { - self = .defaults - self.preferHuffmanEncoding = configuration.preferHuffmanEncoding - } -} diff --git a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift index c150762..1069a9d 100644 --- a/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift +++ b/Sources/NIOHTTPServer/Configuration/HTTP3/NIOHTTPServerConfiguration+HTTP3.swift @@ -13,47 +13,50 @@ //===----------------------------------------------------------------------===// import NIOCore +import NIOHTTP3 @available(anyAppleOS 26.0, *) extension NIOHTTPServerConfiguration { /// Configuration for HTTP/3. public struct HTTP3: Sendable, Hashable { + /// If true, Huffman encoding will be used where applicable, e.g. for header field sections. + /// + /// - Note: Huffman encoding will not be used if it would result in a larger payload than not using it, even if + /// this property is true. + public var preferHuffmanEncoding = true + /// QUIC transport configuration. public var quicConfiguration: QUICConfiguration = .defaults - /// HTTP/3 protocol-level configuration. - public var protocolConfiguration: ProtocolConfiguration = .defaults - /// HTTP/3 connection settings exchanged with the client during connection establishment. public var connectionSettings: ConnectionSettings = .defaults - /// Creates an HTTP/3 configuration with the given HTTP/3 protocol configuration, connection settings, and QUIC - /// transport configuration. + /// Creates an HTTP/3 configuration. /// /// - Parameters: + /// - preferHuffmanEncoding: Whether Huffman encoding is used where applicable. /// - quicConfiguration: QUIC transport parameters. - /// - protocolConfiguration: Settings that control HTTP/3 protocol behaviour. /// - connectionSettings: HTTP/3 connection-level settings exchanged with the client. public init( + preferHuffmanEncoding: Bool, quicConfiguration: QUICConfiguration, - protocolConfiguration: ProtocolConfiguration, connectionSettings: ConnectionSettings, ) { - self.protocolConfiguration = protocolConfiguration - self.connectionSettings = connectionSettings + self.preferHuffmanEncoding = preferHuffmanEncoding self.quicConfiguration = quicConfiguration + self.connectionSettings = connectionSettings } /// The default HTTP/3 configuration. /// /// Uses the default configurations of the sub-components: + /// - `preferHuffmanEncoding`: `true`. /// - `quicConfiguration`: ``QUICConfiguration/defaults``. - /// - `protocolConfiguration`: ``ProtocolConfiguration/defaults``. /// - `connectionSettings`: ``ConnectionSettings/defaults``. public static var defaults: Self { Self( + preferHuffmanEncoding: true, quicConfiguration: .defaults, - protocolConfiguration: .defaults, connectionSettings: .defaults, ) } @@ -64,3 +67,11 @@ extension NIOHTTPServerConfiguration { } } } + +@available(anyAppleOS 26.0, *) +extension NIOHTTP3.HTTP3ServerConfiguration { + init(_ configuration: NIOHTTPServerConfiguration.HTTP3) { + self = .defaults + self.preferHuffmanEncoding = configuration.preferHuffmanEncoding + } +} diff --git a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift index d6cfb27..a42a063 100644 --- a/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift +++ b/Tests/NIOHTTPServerTests/HTTP3ConfigurationTests.swift @@ -19,21 +19,17 @@ import Testing @Suite struct HTTP3ConfigurationTests { - @Test("HTTP/3 default configuration uses the defaults of the three sub-components") + @Test("HTTP/3 default configuration uses the defaults of the sub-components") @available(anyAppleOS 26.0, *) func http3DefaultConfiguration() { let config = NIOHTTPServerConfiguration.HTTP3.defaults #expect(config.quicConfiguration == .defaults) - #expect(config.protocolConfiguration == .defaults) #expect(config.connectionSettings == .defaults) } @Test("HTTP/3 configuration with custom values") @available(anyAppleOS 26.0, *) func http3ConfigurationCustomValues() { - var protocolConfiguration = NIOHTTPServerConfiguration.HTTP3.ProtocolConfiguration.defaults - protocolConfiguration.preferHuffmanEncoding = false - var connectionSettings = NIOHTTPServerConfiguration.HTTP3.ConnectionSettings.defaults connectionSettings.qpackMaximumTableCapacity = 4096 connectionSettings.qpackBlockedStreams = 16 @@ -44,12 +40,12 @@ struct HTTP3ConfigurationTests { quic.sendRetry = true let config = NIOHTTPServerConfiguration.HTTP3( + preferHuffmanEncoding: false, quicConfiguration: quic, - protocolConfiguration: protocolConfiguration, connectionSettings: connectionSettings ) - #expect(config.protocolConfiguration.preferHuffmanEncoding == false) + #expect(config.preferHuffmanEncoding == false) #expect(config.connectionSettings.qpackMaximumTableCapacity == 4096) #expect(config.connectionSettings.qpackBlockedStreams == 16) #expect(config.connectionSettings.maximumFieldSectionSize == 8192) From e3ac73df577602b555d450c0c1a7307999381b7c Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 16 Jul 2026 15:04:42 +0100 Subject: [PATCH 5/6] Add HTTP/3 to supported HTTP versions config --- .../NIOHTTPServerConfiguration.swift | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift index 2f462e3..4a5ce10 100644 --- a/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift +++ b/Sources/NIOHTTPServer/Configuration/NIOHTTPServerConfiguration.swift @@ -463,16 +463,29 @@ extension NIOHTTPServerConfiguration { enum Version { case http1_1 case http2(config: HTTP2) + case http3(config: HTTP3) - /// The HTTP/2 configuration if this version is HTTP/2, or `nil` if it is HTTP/1.1. + /// The HTTP/2 configuration if this version is HTTP/2, or `nil` if it is HTTP/1.1 or HTTP/3. var http2Config: HTTP2? { switch self { - case .http1_1: + case .http1_1, .http3: return nil + case .http2(let config): return config } } + + /// The HTTP/3 configuration if this version is HTTP/3, or `nil` if it is HTTP/1.1 or HTTP/2. + var http3Config: HTTP3? { + switch self { + case .http1_1, .http2: + return nil + + case .http3(let config): + return config + } + } } let version: Version @@ -489,11 +502,18 @@ extension NIOHTTPServerConfiguration { Self(version: .http2(config: config)) } + /// The HTTP/3 protocol version. + /// + /// - Parameter config: The configuration to use for HTTP/3. + public static func http3(config: HTTP3) -> Self { + Self(version: .http3(config: config)) + } + /// Two values are equal if they represent the same protocol version, regardless of any differences in HTTP/2 /// configuration. public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs.version, rhs.version) { - case (.http1_1, .http1_1), (.http2, .http2): + case (.http1_1, .http1_1), (.http2, .http2), (.http3, .http3): return true default: @@ -509,6 +529,9 @@ extension NIOHTTPServerConfiguration { case .http2: hasher.combine(2) + + case .http3: + hasher.combine(3) } } } @@ -543,8 +566,15 @@ extension Set where Element == NIOHTTPServerConfiguration.HTTPVersion { return identifiers } - /// The HTTP/2 configuration if HTTP/2 is among the supported versions, or `nil` if only HTTP/1.1 is supported. + /// The HTTP/2 configuration if HTTP/2 is among the supported versions, or `nil` if only HTTP/1.1 and/or HTTP/3 is + /// supported. var http2ConfigIfSupported: NIOHTTPServerConfiguration.HTTP2? { self.compactMap({ $0.version.http2Config }).first } + + /// The HTTP/3 configuration if HTTP/3 is among the supported versions, or `nil` if only HTTP/1.1 and/or HTTP/2 is + /// supported. + var http3ConfigIfSupported: NIOHTTPServerConfiguration.HTTP3? { + self.compactMap({ $0.version.http3Config }).first + } } From 40dc5f7f14c07fa1e5d1e2f83b63672f28bd8efc Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 16 Jul 2026 17:49:23 +0100 Subject: [PATCH 6/6] Remove custom `apibreakage.yml` and use updated upstream --- .github/workflows/apibreakage.yml | 35 ------------------------------ .github/workflows/pull_request.yml | 5 +++-- 2 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/apibreakage.yml diff --git a/.github/workflows/apibreakage.yml b/.github/workflows/apibreakage.yml deleted file mode 100644 index 95382e5..0000000 --- a/.github/workflows/apibreakage.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: api breakage checks - -permissions: - contents: read - -on: - workflow_call: - -jobs: - api-breakage-check: - name: API breakage check - runs-on: ubuntu-latest - container: - image: swiftlang/swift:nightly-6.4.x-noble - timeout-minutes: 40 - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - persist-credentials: false - fetch-depth: 0 # Fetching tags requires fetch-depth: 0 (https://github.com/actions/checkout/issues/1471) - - name: Mark the workspace as safe - # https://github.com/actions/checkout/issues/766 - run: git config --global --add safe.directory ${GITHUB_WORKSPACE} - - name: Run API breakage check - shell: bash - # While depending on swift-crypto beta releases, an environment variable is required - # to allow swift-certificates to accept the beta versions. - env: - SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA: 1 - run: | - git fetch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} ${GITHUB_BASE_REF}:pull-base-ref - BASELINE_REF='pull-base-ref' - echo "Using baseline: $BASELINE_REF" - swift package diagnose-api-breaking-changes "$BASELINE_REF" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c19c21e..6f62569 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,11 +10,12 @@ on: jobs: soundness: name: Soundness - uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.11 + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@497d9ab8410e69981b3b2f28da24cf7bf4b8733d with: - api_breakage_check_enabled: false + api_breakage_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" format_check_container_image: "swiftlang/swift:nightly-6.4.x-noble" license_header_check_project_name: "Swift HTTP Server" + linux_pre_build_command: "export SWIFT_CERTIFICATES_ALLOW_SWIFT_CRYPTO_BETA=1" unit-tests: name: Unit tests