Skip to content

Add APIs for reading/writing HTTP Datagrams - #106

Open
aryan-25 wants to merge 5 commits into
swift-server:mainfrom
aryan-25:unreliable-datagrams
Open

Add APIs for reading/writing HTTP Datagrams#106
aryan-25 wants to merge 5 commits into
swift-server:mainfrom
aryan-25:unreliable-datagrams

Conversation

@aryan-25

Copy link
Copy Markdown
Collaborator

Motivation

Users may wish to send/receive HTTP Datagrams (RFC 9297) using NIOHTTPServer. This PR introduces reader and writer APIs for unreliable datagrams.

Note that the underlying support is yet to be provided by swift-nio-http3. The read/write source is not wired up, and we currently throw an error when attempting to read or write to/from the unreliable datagram stream.

Modifications

  • Added an UnstableHTTPDatagrams trait, disabled by default. All new APIs are behind that trait (note that the HTTP3 trait must also be enabled as unreliable datagrams are currently only supported over QUIC).

  • Added a withDatagramReader method on NIOHTTPServer.Reader and a withDatagramWriter method on NIOHTTPServer.ResponseSender.Writer. Both methods are consuming and take a closure that receive the reliable request/response body reader/writer and optionally the unreliable datagram reader/writer as arguments.

    • In both methods, the readers/writers are passed as consuming sending, so callers can run the readers/writers separately in concurrent tasks.
  • Added ConnectUDPExample.swift to show the proposed shape end to end.

Result

APIs for sending/receiving unreliable HTTP Datagrams added.

### Motivation

Users may wish to send/receive HTTP Datagrams ([RFC 9297](https://datatracker.ietf.org/doc/html/rfc9297)) using NIOHTTPServer. This PR introduces reader and writer APIs for unreliable datagrams.

Note that the underlying support is yet to be provided by `swift-nio-http3`. The read/write source is not wired up, and we currently throw an error when attempting to read or write to/from the unreliable datagram stream.

### Modifications

- Added an `UnstableHTTPDatagrams` trait, disabled by default. All new APIs are behind that trait (note that the `HTTP3` trait must also be enabled as unreliable datagrams are currently only supported over QUIC).

- Added a `withDatagramReader` method on `NIOHTTPServer.Reader` and a `withDatagramWriter` method on `NIOHTTPServer.ResponseSender.Writer`. Both methods are `consuming` and take a closure that receive the reliable request/response body reader/writer and optionally the unreliable datagram reader/writer as arguments.
  - In both methods, the readers/writers are passed as `consuming sending`, so callers can run the readers/writers separately in concurrent tasks.

- Added `ConnectUDPExample.swift` to show the proposed shape end to end.

### Result

APIs for sending/receiving unreliable HTTP Datagrams added.
@aryan-25
aryan-25 requested review from ehaydenr and gjcairo July 31, 2026 09:25
@aryan-25 aryan-25 added the 🆕 semver/minor Adds new public API. label Jul 31, 2026
#if HTTP3 && UnstableHTTPDatagrams
/// The unreliable datagram reader, present when the underlying transport is capable of reading/writing
/// unreliable datagrams.
private var datagramReader: Disconnected<NIOHTTPServer.DatagramReader>?

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.

Do we need Disconnected?

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, without this, the datagramReader can't be vended out as sending in withDatagramReader.

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 see.

I am quite unconvinced about storing this reader inside the stream reader to be honest - I think it would make more sense to have this somewhere else, like the request context. But I'm also aware this will likely be temporary until we can come up with a more general solution to CONNECT.

public consuming func withDatagramWriter(
_ body: (consuming sending Self, consuming sending NIOHTTPServer.DatagramWriter?) async throws -> Void
) async throws {
let streamFinish = NIOHTTPServer.StreamFinish(writer: self.writer, state: self.writerState)

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 think this type is a bit unnecessary and adds some indirection which makes things harder to follow, IMO. I'd just finish both streams inline in this func, like we do elsewhere.

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.

We unfortunately need this indirection because we pass the stream writer (and the datagram writer) as consuming sending to the body closure, which means we can't call finish on either after the body closure.

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.

This is actually making me doubt we need to have a with-style method (also on the reader). I don't think we should finish the writers implicitly - we don't do this for the normal stream writer: we just expose write and finish. Why not return a tuple with both writers (and readers) instead of having a closure?

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.

Regardless of whether we should automatically finish or not

Why not return a tuple with both writers (and readers) instead of having a closure?

This won't work because tuples with non-Copyable elements aren't supported yet.

I think the closure shape is the only shape that allows both the readers/writers to be run in concurrent tasks.

Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerReaderTests.swift Outdated
@aryan-25
aryan-25 requested a review from gjcairo July 31, 2026 12:25
#if HTTP3 && UnstableHTTPDatagrams
/// The unreliable datagram reader, present when the underlying transport is capable of reading/writing
/// unreliable datagrams.
private var datagramReader: Disconnected<NIOHTTPServer.DatagramReader>?

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 see.

I am quite unconvinced about storing this reader inside the stream reader to be honest - I think it would make more sense to have this somewhere else, like the request context. But I'm also aware this will likely be temporary until we can come up with a more general solution to CONNECT.

///
/// - Note: Both readers are passed as `consuming sending`. This means that `body` can use both readers in separate
/// tasks.
public consuming func withDatagramReader(

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.

Related to my last comment, some things about this function I'm not sure about. The fact we need to create a new Reader isn't great. Users would also be able to call withDatagramReader again on the streamReader that's returned (since it's the same type), which is a bit weird. Again, perhaps it's fine since this will be temporary, but I thought I'd point them out.

public consuming func withDatagramWriter(
_ body: (consuming sending Self, consuming sending NIOHTTPServer.DatagramWriter?) async throws -> Void
) async throws {
let streamFinish = NIOHTTPServer.StreamFinish(writer: self.writer, state: self.writerState)

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.

This is actually making me doubt we need to have a with-style method (also on the reader). I don't think we should finish the writers implicitly - we don't do this for the normal stream writer: we just expose write and finish. Why not return a tuple with both writers (and readers) instead of having a closure?

@aryan-25 aryan-25 left a comment

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 revised shape looks good. Just some small comments. Thanks for driving this forward @gjcairo!

Comment thread Sources/NIOHTTPServer/NIOHTTPServerResponseSender.swift Outdated
Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerReaderTests.swift Outdated
Comment thread Sources/NIOHTTPServer/Datagrams/NIOHTTPServer+Datagrams.swift Outdated
Comment thread Sources/NIOHTTPServer/NIOHTTPServerReader.swift Outdated
Comment thread Sources/NIOHTTPServer/NIOHTTPServer+ConnectionContext.swift Outdated
@aryan-25

Copy link
Copy Markdown
Collaborator Author

LGTM, thank you!

@gjcairo
gjcairo enabled auto-merge (squash) August 13, 2026 15:35
extension NIOHTTPServer {
/// A reader for the unreliable datagram stream.
public struct DatagramReader: AsyncReader, ~Copyable {
public typealias ReadElement = UInt8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the element be a datagram instead of a byte?

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

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants