Skip to content
Open
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
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ var traits: Set<Trait> = [
name: "HTTP3",
description: "Enables HTTP/3 support"
),
.trait(
name: "UnstableHTTPDatagrams",
description: "Enables support for reading and writing unreliable HTTP datagrams"
),
]

let defaultTraits: Set<String> = ["Configuration"]
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Available traits:
- **`Configuration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration`
`ConfigProvider`.
- **`HTTP3`**: Enables HTTP/3 support.
- **`UnstableHTTPDatagrams`**: Enables support for reading and writing unreliable HTTP datagrams. Note that the `HTTP3`
trait must be enabled alongside.

## HTTP/3 support

Expand Down
173 changes: 173 additions & 0 deletions Sources/NIOHTTPServer/Datagrams/ConnectUDPExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//===----------------------------------------------------------------------===//
//
// 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 BasicContainers
import HTTPAPIs
import NIOCore
import NIOHTTPTypes
import NetworkTypes

@available(anyAppleOS 26.0, *)
func connectUDPExample(
request: HTTPRequest,
context: NIOHTTPServer.ConnectionContext,
reader: consuming sending NIOHTTPServer.Reader,
responseSender: consuming sending NIOHTTPServer.ResponseSender
) async throws {
guard ConnectUDPHelper.isValidConnectUDPRequest(request, version: context.httpVersion) else {
return try await responseSender.sendAndFinish(.init(status: .forbidden))
}

var disconnectedResponseSender = Disconnected(value: Optional(responseSender))

try await reader.withDatagramReader { streamReader, maybeDatagramReader in
let responseSender = disconnectedResponseSender.swap(newValue: nil)!

// The unreliable datagram transport will not be available if the underlying transport does not support
// unreliable datagrams, like in HTTP/1.1 and HTTP/2 over TCP, or also over HTTP/3 when support for datagrams is
// not negotiated, i.e. we (the server) either sent or received the `SETTINGS_H3_DATAGRAM` setting with value 0.
//
// Since this example wants to showcase the unreliable datagram reader/writer APIs, we just return early if the
// unreliable datagram transport is not available. However, note that in these cases, it is still possible to
// perform CONNECT-UDP by exchanging data through the Capsule protocol over the request/response reader/writer.
guard var datagramReader = maybeDatagramReader else {
return try await responseSender.sendAndFinish(.init(status: .notImplemented))
}

// Store any bytes we read before sending the response so we can send them to the target.
var pendingToTarget: [UInt8] = []

var streamReader = streamReader
try await streamReader.read { buffer, _ in
for index in buffer.indices { pendingToTarget.append(buffer[index]) }
}

try await datagramReader.read { buffer, _ in
for index in buffer.indices { pendingToTarget.append(buffer[index]) }
}

// Hold the readers until the tunnel is established.
var disconnectedStreamReader = Disconnected(value: Optional(streamReader))
var disconnectedDatagramReader = Disconnected(value: Optional(datagramReader))

// Now accept the request and access the datagram writer through the response writer.
let writer = try await responseSender.send(ConnectUDPHelper.makeSuccessResponse(version: context.httpVersion))
try await writer.withDatagramWriter { streamWriter, datagramWriter in
var disconnectedStreamWriter = Disconnected(value: Optional(streamWriter))
var disconnectedDatagramWriter = Disconnected(value: datagramWriter)

await withThrowingTaskGroup { group in
var unwrappedStreamWriter = disconnectedStreamWriter.swap(newValue: nil)!
var unwrappedStreamReader = disconnectedStreamReader.swap(newValue: nil)!

// Write to the reliable stream.
group.addTask {
var emptyBuffer = UniqueArray<UInt8>()
try await unwrappedStreamWriter.write(buffer: &emptyBuffer)
}

if var unwrappedDatagramWriter = disconnectedDatagramWriter.swap(newValue: nil) {
// Write to the unreliable stream.
group.addTask {
var emptyBuffer = UniqueArray<UInt8>()
try await unwrappedDatagramWriter.write(buffer: &emptyBuffer)
}
}

// Read from the reliable stream.
group.addTask {
try await unwrappedStreamReader.read { _, _ in
()
}
}

if var unwrappedDatagramReader = disconnectedDatagramReader.swap(newValue: nil) {
// Read from the unreliable stream.
group.addTask {
try await unwrappedDatagramReader.read { _, _ in
()
}
}
}
}
}
}
}

@available(anyAppleOS 26.0, *)
enum ConnectUDPHelper {
/// Validate that `request` corresponds to a valid CONNECT-UDP request.
static func isValidConnectUDPRequest(_ request: HTTPRequest, version: NIOHTTPServer.HTTPVersion) -> Bool {
guard request.method == .connect else {
return false
}

switch version {
case .plaintextHTTP1_1, .http1_1:
let hasConnectionUpgrade = request.headerFields[.connection]?.lowercased() == "upgrade"
let hasUpgradeConnectUDP = request.headerFields[.upgrade] == "connect-udp"

guard hasConnectionUpgrade, hasUpgradeConnectUDP else {
return false
}

case .http2:
guard request.extendedConnectProtocol == "connect-udp" else {
return false
}

#if HTTP3
case .http3:
guard request.extendedConnectProtocol == "connect-udp" else {
return false
}
#endif
}

return true
}

/// Returns a success response to accept the tunnel.
static func makeSuccessResponse(version: NIOHTTPServer.HTTPVersion) -> HTTPResponse {
switch version {
case .plaintextHTTP1_1, .http1_1:
HTTPResponse(
status: .switchingProtocols,
headerFields: [
.connection: "Upgrade",
.upgrade: "connect-udp",
.capsuleProtocol: "?1",
]
)

case .http2:
HTTPResponse(status: .ok, headerFields: [.capsuleProtocol: "?1"])

#if HTTP3
case .http3:
HTTPResponse(status: .ok, headerFields: [.capsuleProtocol: "?1"])
#endif
}
}
}

extension HTTPField.Name {
static var capsuleProtocol: Self {
Self("Capsule-Protocol")!
}
}

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

public import BasicContainers
public import HTTPAPIs
import NIOCore
import NIOHTTPTypes
import Synchronization

/// Errors from reading/writing on the unreliable datagram.
@available(anyAppleOS 26.0, *)
public enum DatagramsError: Error, Sendable {
/// The unreliable datagram transport is not yet implemented.
case notImplemented
}

@available(anyAppleOS 26.0, *)
extension NIOHTTPServer {
/// A reader for the unreliable datagram stream.
public struct DatagramReader: AsyncReader, ~Copyable {
public typealias ReadElement = UInt8
public typealias Buffer = UniqueArray<UInt8>
public typealias ReadFailure = any Error
public typealias FinalElement = Void

public mutating func read<Return: ~Copyable, Failure: Error>(
body: (inout Buffer, consuming FinalElement?) async throws(Failure) -> Return
) async throws(EitherError<ReadFailure, Failure>) -> Return {
// TODO: The datagram transport is not yet implemented.
throw .first(DatagramsError.notImplemented)
}
}

/// A writer for the unreliable datagram stream.
public struct DatagramWriter: CallerAsyncWriter, ~Copyable {
public typealias WriteElement = UInt8
public typealias WriteFailure = any Error
public typealias FinalElement = Void

public mutating func write<Buffer: RangeReplaceableContainer<WriteElement> & ~Copyable>(
buffer: inout Buffer
) async throws where Buffer.Element: ~Copyable {
// TODO: The datagram transport is not yet implemented.
throw DatagramsError.notImplemented
}

public consuming func finish<Buffer: RangeReplaceableContainer<WriteElement> & ~Copyable>(
buffer: inout Buffer,
finalElement: consuming Void
) async throws where Buffer.Element: ~Copyable {
// TODO: The datagram transport is not yet implemented.
throw DatagramsError.notImplemented
}
}
}

@available(*, unavailable)
extension NIOHTTPServer.DatagramReader: Sendable {}

@available(*, unavailable)
extension NIOHTTPServer.DatagramWriter: Sendable {}

@available(anyAppleOS 26.0, *)
extension NIOHTTPServer {
struct StreamFinish: ~Copyable {
let writer: NIOAsyncChannelOutboundWriter<HTTPResponsePart>
let state: NIOHTTPServer.ResponseSender.WriterState

consuming func finish() async throws {
// Check if `finish` has not already been fired.
let shouldClose = self.state.wrapped.withLock { !$0.finishedWriting }
guard shouldClose else { return }

try await self.writer.write(.end(nil))
self.state.wrapped.withLock { $0.finishedWriting = true }
}
}
}

#endif // HTTP3 && UnstableHTTPDatagrams
19 changes: 19 additions & 0 deletions Sources/NIOHTTPServer/NIOHTTPServer+ConnectionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ extension NIOHTTPServer {
self.peerCertificateChainFuture = peerCertificateChainFuture
}

#if HTTP3 && UnstableHTTPDatagrams
/// Whether this connection supports unreliable datagrams.
public var supportsUnreliableDatagrams: Bool {
switch self.httpVersion {
#if HTTP3
case .http3:
// TODO: Until `swift-nio-http3` exposes a mechanism for retrieving the unreliable datagram reader and
// writer, we return `false` here. Additionally, note that even over HTTP/3, it is not always guaranteed
// that the unreliable datagram reader/writer is available; the support must be negotiated with the peer
// through the `SETTINGS_H3_DATAGRAM` setting.
false
#endif

case .plaintextHTTP1_1, .http1_1, .http2:
false
}
}
#endif

/// The peer's validated certificate chain. Returns `nil` if a custom
/// verification callback was not set when configuring mTLS in the
/// server configuration, or if the custom verification callback did not
Expand Down
7 changes: 7 additions & 0 deletions Sources/NIOHTTPServer/NIOHTTPServer+RequestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ extension NIOHTTPServer {
init(connectionContext: ConnectionContext) {
self.connectionContext = connectionContext
}

#if HTTP3 && UnstableHTTPDatagrams
/// Whether this connection supports unreliable datagrams.
public var supportsUnreliableDatagrams: Bool {
self.connectionContext.supportsUnreliableDatagrams
}
#endif
}
}

Expand Down
16 changes: 12 additions & 4 deletions Sources/NIOHTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,22 @@ public struct NIOHTTPServer: HTTPServer {
let readerState = Reader.ReaderState(iterator: iterator)
let writerState = ResponseSender.WriterState()

#if HTTP3 && UnstableHTTPDatagrams
// TODO: `swift-nio-http3` currently does not provide APIs for reading/writing bytes on the unreliable datagram
// stream. This is why we currently pass `nil` to the `datagramReader` and `datagramWriter` arguments.
let requestReader = Reader(readerState: readerState, datagramReader: nil)
let responseSender = ResponseSender(writer: outbound, writerState: writerState, datagramWriter: nil)
#else
let requestReader = Reader(readerState: readerState)
let responseSender = ResponseSender(writer: outbound, writerState: writerState)
#endif

do {
try await handler.handle(
request: request,
requestContext: RequestContext(connectionContext: context),
reader: Reader(
readerState: readerState
),
responseSender: ResponseSender(writer: outbound, writerState: writerState)
reader: requestReader,
responseSender: responseSender
)
} catch {
logger.error("Error thrown while handling request: \(error)")
Expand Down
Loading
Loading