Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "2.101.3"),
.package(url: "https://github.com/apple/swift-nio-quic.git", .upToNextMinor(from: "0.2.1")),
.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.2.0")),
// TODO: Update once datagram APIs are released.
.package(url: "https://github.com/apple/swift-nio-http3.git", branch: "main"),
.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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,31 @@ extension NIOHTTPServerConfiguration.HTTP3 {
/// `SETTINGS_MAX_FIELD_SECTION_SIZE`.
public var maximumFieldSectionSize: UInt64?

/// Whether the server should advertise support for receiving HTTP/3 datagrams.
///
/// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9297.html#section-2.1.1-1. Corresponds to
/// `SETTINGS_H3_DATAGRAM`.
var http3Datagram: Bool

init(
qpackMaximumTableCapacity: UInt64,
qpackBlockedStreams: UInt64,
maximumFieldSectionSize: UInt64?
) {
self.qpackMaximumTableCapacity = qpackMaximumTableCapacity
self.qpackBlockedStreams = qpackBlockedStreams
self.maximumFieldSectionSize = maximumFieldSectionSize
// Set `http3Datagram` to `true`. This will later be updated in the `NIOHTTPServerConfiguration.HTTP3`
// callsite to stay consistent with the QUIC configuration.
self.http3Datagram = true
}

/// The default HTTP/3 connection settings configuration.
///
/// Uses the following default values:
/// - `qpackMaximumTableCapacity`: 0.
/// - `qpackBlockedStreams`: 0.
/// - `maximumFieldSectionSize`: `nil` (no field section size limit).
/// - `qpackMaximumTableCapacity`: 0
/// - `qpackBlockedStreams`: 0
/// - `maximumFieldSectionSize`: `nil` (no field section size limit)
public static var defaults: Self {
Self(
qpackMaximumTableCapacity: 0,
Expand All @@ -59,7 +78,8 @@ extension HTTP3.HTTP3Settings {
self.init(
qpackMaximumTableCapacity: configuration.qpackMaximumTableCapacity,
qpackBlockedStreams: configuration.qpackBlockedStreams,
maximumFieldSectionSize: configuration.maximumFieldSectionSize
maximumFieldSectionSize: configuration.maximumFieldSectionSize,
h3Datagram: configuration.http3Datagram
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#if HTTP3 && UnstableHTTPDatagrams
@available(anyAppleOS 26.0, *)
extension NIOHTTPServerConfiguration.HTTP3 {
public struct DatagramConfiguration: Sendable, Hashable {
/// The maximum datagram frame size in bytes.
public var maxDatagramFrameSize: Int {
didSet {
self.validateMaxDatagramFrameSize()
}
}

private func validateMaxDatagramFrameSize() {
precondition(
self.maxDatagramFrameSize != 0,
"When maxDatagramFrameSize == 0, support for receiving HTTP/3 datagrams is disabled. Set `datagramConfiguration` to `nil` if you do not want to receive datagrams."
)
}

/// The maximum number of inbound HTTP/3 datagrams that will be buffered for each stream.
public var maxBufferedDatagrams: Int

init(maxDatagramFrameSize: Int, maxBufferedDatagrams: Int) {
self.maxDatagramFrameSize = maxDatagramFrameSize
self.maxBufferedDatagrams = maxBufferedDatagrams

self.validateMaxDatagramFrameSize()
}

/// The default HTTP/3 datagram configuration. Uses the following default values:
///
/// - `maxDatagramFrameSize`: 65535
/// - `maxBufferedDatagrams`: 16
public static var defaults: Self {
Self(maxDatagramFrameSize: 65535, maxBufferedDatagrams: 16)
}
}
}
#endif // HTTP3 && UnstableHTTPDatagrams
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,43 @@ extension NIOHTTPServerConfiguration.HTTP3 {
/// debugging and analysis.
public var qLogConfiguration: QLogConfiguration?

/// The maximum datagram frame size in bytes. If set to 0, the server will not advertise support for receiving
/// unreliable datagrams.
var maxDatagramFrameSize: Int

init(
serverName: String,
keyExchangeGroup: KeyExchangeGroup,
maxIdleTimeout: Duration,
initialMaxData: Int,
initialMaxStreamDataBidirectionalLocal: Int,
initialMaxStreamDataBidirectionalRemote: Int,
initialMaxStreamDataUnidirectional: Int,
initialMaxStreamsBidirectional: Int,
initialMaxStreamsUnidirectional: Int,
keepAliveInterval: Duration? = nil,
sendRetry: Bool,
keyLogPath: String? = nil,
qLogConfiguration: QLogConfiguration? = nil
) {
self.serverName = serverName
self.keyExchangeGroup = keyExchangeGroup
self.maxIdleTimeout = maxIdleTimeout
self.initialMaxData = initialMaxData
self.initialMaxStreamDataBidirectionalLocal = initialMaxStreamDataBidirectionalLocal
self.initialMaxStreamDataBidirectionalRemote = initialMaxStreamDataBidirectionalRemote
self.initialMaxStreamDataUnidirectional = initialMaxStreamDataUnidirectional
self.initialMaxStreamsBidirectional = initialMaxStreamsBidirectional
self.initialMaxStreamsUnidirectional = initialMaxStreamsUnidirectional
self.keepAliveInterval = keepAliveInterval
self.sendRetry = sendRetry
self.keyLogPath = keyLogPath
self.qLogConfiguration = qLogConfiguration
// Set `maxDatagramFrameSize` to 65535. This will later be updated in the
// `NIOHTTPServerConfiguration.HTTP3` callsite to stay consistent with the HTTP/3 configuration.
self.maxDatagramFrameSize = 65535
}

/// The default QUIC transport configuration.
///
/// Uses the following default values:
Expand Down Expand Up @@ -275,7 +312,8 @@ extension NIOQUIC.QUICConfiguration {
keepAliveInterval: config.keepAliveInterval,
sendRetry: config.sendRetry,
keyLogPath: config.keyLogPath,
qLogConfiguration: config.qLogConfiguration.map { .init($0) }
qLogConfiguration: config.qLogConfiguration.map { .init($0) },
maxDatagramFrameSize: config.maxDatagramFrameSize
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,89 @@ extension NIOHTTPServerConfiguration {
/// HTTP/3 connection settings exchanged with the client during connection establishment.
public var connectionSettings: ConnectionSettings = .defaults

#if UnstableHTTPDatagrams
/// The HTTP/3 datagram configuration. If set to `nil`, the server will not advertise support for receiving
/// HTTP/3 datagrams.
public var datagramConfiguration: DatagramConfiguration? = .defaults {
didSet {
self.updateDatagramConfiguration()
}
}
#endif // UnstableHTTPDatagrams

private mutating func updateDatagramConfiguration() {
// Update `self.quicConfiguration` and `self.connectionSettings` when the value changes.
if let datagramConfig = self.datagramConfiguration {
self.quicConfiguration.maxDatagramFrameSize = datagramConfig.maxDatagramFrameSize
self.connectionSettings.http3Datagram = true
} else {
// Set `maxDatagramFrameSize` to 0 and `http3Datagram` to `false` so that the server doesn't
// advertise support for receiving datagrams.
self.quicConfiguration.maxDatagramFrameSize = 0
self.connectionSettings.http3Datagram = false
}
}

#if UnstableHTTPDatagrams
/// Creates an HTTP/3 configuration.
///
/// - Parameters:
/// - preferHuffmanEncoding: Whether Huffman encoding is used where applicable.
/// - quicConfiguration: QUIC transport parameters.
/// - connectionSettings: HTTP/3 connection-level settings exchanged with the client.
/// - datagramConfiguration: The HTTP/3 datagram configuration. If set to `nil`, the server will not advertise
/// support for receiving HTTP/3 datagrams.
public init(
preferHuffmanEncoding: Bool,
quicConfiguration: QUICConfiguration,
connectionSettings: ConnectionSettings,
datagramConfiguration: DatagramConfiguration? = .defaults
) {
self.preferHuffmanEncoding = preferHuffmanEncoding
self.quicConfiguration = quicConfiguration
self.connectionSettings = connectionSettings
self.datagramConfiguration = datagramConfiguration
}
#else
/// Creates an HTTP/3 configuration.
///
/// - Parameters:
/// - preferHuffmanEncoding: Whether Huffman encoding is used where applicable.
/// - quicConfiguration: QUIC transport parameters.
/// - connectionSettings: HTTP/3 connection-level settings exchanged with the client.
public init(
preferHuffmanEncoding: Bool,
quicConfiguration: QUICConfiguration,
connectionSettings: ConnectionSettings,
) {
self.preferHuffmanEncoding = preferHuffmanEncoding
self.quicConfiguration = quicConfiguration
self.connectionSettings = connectionSettings
}
#endif // UnstableHTTPDatagrams

/// The default HTTP/3 configuration.
///
/// Uses the default configurations of the sub-components:
/// - `preferHuffmanEncoding`: `true`.
/// - `quicConfiguration`: ``QUICConfiguration/defaults``.
/// - `connectionSettings`: ``ConnectionSettings/defaults``.
/// - `datagramConfiguration`: ``DatagramConfiguration/defaults``.
public static var defaults: Self {
#if UnstableHTTPDatagrams
Self(
preferHuffmanEncoding: true,
quicConfiguration: .defaults,
connectionSettings: .defaults,
datagramConfiguration: .defaults
)
#else
Self(
preferHuffmanEncoding: true,
quicConfiguration: .defaults,
connectionSettings: .defaults
)
#endif // UnstableHTTPDatagrams
}

// The fallback connection RTT to use if there is an error obtaining the RTT estimate channel option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,36 @@ extension NIOHTTPServerConfiguration.HTTP3 {
/// Initialize an HTTP/3 configuration from a config reader.
///
/// ## Configuration keys:
/// HTTP/3 configuration contains three sub-scopes. All keys are optional and resolve to their default values if not
/// HTTP/3 configuration contains four sub-scopes. All keys are optional and resolve to their default values if not
/// provided:
/// - ``NIOHTTPServerConfiguration/HTTP3/defaults``
/// - ``NIOHTTPServerConfiguration/HTTP3/ConnectionSettings/defaults``
/// - ``NIOHTTPServerConfiguration/HTTP3/QUICConfiguration/defaults``.
/// - ``NIOHTTPServerConfiguration/HTTP3/DatagramConfiguration/defaults``.
///
/// - **`"protocolConfiguration"`**: HTTP/3 protocol-level settings (see ``ProtocolConfiguration/init(config:)``).
/// - **`"connectionSettings"`**: HTTP/3 connection settings exchanged with the client (see
/// ``ConnectionSettings/init(config:)``).
/// - **`"quicConfiguration"`**: QUIC transport configuration (see ``QUICConfiguration/init(config:)``).
/// - **`"datagramConfiguration"`**: HTTP/3 datagram configuration (see ``DatagramConfiguration/init(config:)``).
/// Note that the `UnstableHTTPDatagrams` trait must be enabled for this configuration to have any effect.
///
/// - Parameter config: The configuration reader.
public init(config: ConfigSnapshotReader) throws {
#if UnstableHTTPDatagrams
self.init(
preferHuffmanEncoding: config.bool(forKey: "preferHuffmanEncoding", default: true),
quicConfiguration: try .init(config: config.scoped(to: "quicConfiguration")),
connectionSettings: .init(config: config.scoped(to: "connectionSettings")),
datagramConfiguration: .init(config: config.scoped(to: "datagramConfiguration"))
)
#else
self.init(
preferHuffmanEncoding: config.bool(forKey: "preferHuffmanEncoding", default: true),
quicConfiguration: try .init(config: config.scoped(to: "quicConfiguration")),
connectionSettings: .init(config: config.scoped(to: "connectionSettings"))
)
#endif
}
}

Expand Down Expand Up @@ -198,4 +210,31 @@ extension NIOHTTPServerConfiguration.HTTP3.ConnectionSettings {
)
}
}

#if UnstableHTTPDatagrams
@available(anyAppleOS 26.0, *)
extension NIOHTTPServerConfiguration.HTTP3.DatagramConfiguration {
/// Initialize HTTP/3 connection settings from a config reader.
///
/// ## Configuration keys:
/// - `datagramsEnabled` (bool, optional, default: true): Whether the server should advertise support for receiving
/// HTTP/3 datagrams.
/// - `maxDatagramFrameSize` (int, optional, default: 65535): The maximum datagram frame size in bytes.
/// - `maxBufferedDatagrams` (int, optional, default: 16): The maximum number of inbound HTTP/3 datagrams that will
/// be buffered for each stream.
///
/// - SeeAlso: ``NIOHTTPServerConfiguration/HTTP3/DatagramConfiguration``.
///
/// - Parameter config: The configuration reader.
public init?(config: ConfigSnapshotReader) {
guard config.bool(forKey: "datagramsEnabled", default: true) else { return nil }

self.init(
maxDatagramFrameSize: config.int(forKey: "maxDatagramFrameSize", default: 65535),
maxBufferedDatagrams: config.int(forKey: "maxBufferedDatagrams", default: 16)
)
}
}
#endif // UnstableHTTPDatagrams

#endif // HTTP3 && Configuration
56 changes: 56 additions & 0 deletions Sources/NIOHTTPServer/Datagrams/HTTP3DatagramDemultiplexer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#if HTTP3 && UnstableHTTPDatagrams

import NIOCore
import NIOHTTP3
import NIOQUICHelpers

/// Routes inbound HTTP/3 datagrams to registered ``HTTP3DatagramStream`` instances.
@available(anyAppleOS 26.0, *)
final class HTTP3DatagramDemultiplexer: ChannelInboundHandler {
typealias InboundIn = HTTP3Datagram

/// The ``HTTP3DatagramStream`` instance for each open request stream.
private var datagramStreams: [QUICStreamID: HTTP3UnreliableDatagramStream] = [:]

/// Starts routing datagrams received for `datagramStream.streamID` to the provided `datagramStream`.
///
/// - Precondition: Must only be called on the connection channel's event loop.
func register(datagramStream: HTTP3UnreliableDatagramStream) {
self.datagramStreams[datagramStream.streamID] = datagramStream
}

/// Stops routing datagrams to `streamID`.
///
/// - Precondition: Must only be called on the connection channel's event loop.
func deregister(streamID: QUICStreamID) {
self.datagramStreams.removeValue(forKey: streamID)
}

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let datagram = self.unwrapInboundIn(data)
self.datagramStreams[datagram.streamID]?.receive(datagram.payload)
}

func handlerRemoved(context: ChannelHandlerContext) {
for datagramStream in self.datagramStreams.values {
datagramStream.finish()
}
self.datagramStreams.removeAll()
}
}

#endif // HTTP3 && UnstableHTTPDatagrams
Loading
Loading