Skip to content

Reset streams when throwing from request handler - #115

Merged
FranzBusch merged 6 commits into
swift-server:mainfrom
gjcairo:handle-throw-reset
Aug 20, 2026
Merged

Reset streams when throwing from request handler#115
FranzBusch merged 6 commits into
swift-server:mainfrom
gjcairo:handle-throw-reset

Conversation

@gjcairo

@gjcairo gjcairo commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

This change translates errors thrown from the request handler into stream resets.

Two new protocols have been added: NIOHTTPServerHTTP2StreamResetError and NIOHTTPServerHTTP3StreamResetError, which allow to specify how custom errors map to HTTP/2 stream reset error codes and and HTTP/3 stream reset and stop sending error codes, respectively.

The behaviour of throwing an error changes depending on which protocol the request is being served over:

  • HTTP/1: there are no streams, so the connection is closed. If the head has not yet been sent, it will be amended to include Connection: close.
  • HTTP/2: RST_STREAM is sent over the wire, closing the stream bidirectionally. If the error thrown conforms to NIOHTTPServerHTTP2StreamResetError, the mapped error code will be used; if not, the code will be defaulted to INTERNAL_ERROR (0x02).
  • HTTP/3: STREAM_RESET and STOP_SENDING are both sent to the client, closing the stream bidirectionally. If the error thrown conforms to NIOHTTPServerHTTP3StreamResetError, the mapped error code will be used; if not, the code will be defaulted to H3_INTERNAL_ERROR (0x0102).

@gjcairo gjcairo added the 🆕 semver/minor Adds new public API. label Aug 18, 2026
extension NIOHTTPServer {
/// The application-level HTTP version negotiated for a connection.
public enum HTTPVersion: String, Sendable, Hashable {
public enum HTTPVersion: String, Sendable, Hashable, CaseIterable {

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.

We need to mark this as @nonexhaustive otherwise we won't be able to support a hypothethical HTTP/4.

// A throwing handler signals that the exchange failed. The error is deliberately not propagated to any
// caller: it exists to drive the wire, aborting the exchange with protocol error codes the error can choose
// by conforming to `NIOHTTPServerHTTP2StreamResetError` / `NIOHTTPServerHTTP3StreamResetError`.
self.logger.error(

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.

This must not log at error level. Libraries should only log at info level and below. I think this is still somewhat the normal case so I would recommend logging at trace or debug level.

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 changed this and also updated other logs where we were using error

self.logger.error(
"Error thrown while handling request: aborting.",
metadata: [
"error": "\(error)",

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.

swift-log has new APIs that you can pass an error to. Can we use that instead?

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 updated other instances to use the new API too

"Error thrown while handling request: aborting.",
metadata: [
"error": "\(error)",
"protocol": "\(requestContext.connectionContext.httpVersion)",

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.

I would recommend that we move all the keys into a internal struct called LoggingKeys so we don't have random string literals floating around

/// // ...
/// }
/// ```
public protocol NIOHTTPServerHTTP2StreamResetError: Error {

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.

I would recommend a different name here following what GRPC does https://github.com/grpc/grpc-swift-2/blob/76daaeaf345213fbd1b0ae78121c06036a0ddbfe/Sources/GRPCCore/RPCError.swift#L309

Suggested change
public protocol NIOHTTPServerHTTP2StreamResetError: Error {
public protocol NIOHTTPServerHTTP2StreamResetErrorConvertible: Error {

/// // ...
/// }
/// ```
public protocol NIOHTTPServerHTTP3StreamResetError: Error {

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.

Same here

Suggested change
public protocol NIOHTTPServerHTTP3StreamResetError: Error {
public protocol NIOHTTPServerHTTP3StreamResetErrorConvertible: Error {

//
//===----------------------------------------------------------------------===//

public import NIOHTTP2

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.

I'm not happy about this public import. I would prefer if we can continue to avoid exporting NIO into our public API where possible. We should find a better place for this error to live. Maybe even this package

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.

Fair. I changed the type here to UInt32 to remove the NIO dependency and to leave it up to implementers to decide where they get the errors codes from (I've added an example too showing you can get the errors from the NIO H2 error codes' raw value). Let me know what you think. Like this, we could upstream these protocols to the API proposal package eventually.

(I know we generally expose Int in APIs, but only valid values for H2 steam error codes are positive integers up to 32 bits so it seems better to enforce it like this).

//===----------------------------------------------------------------------===//

#if HTTP3
public import HTTP3

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.

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

I did the same here, but using UInt64, for the same reasons as I explained above.

@gjcairo
gjcairo requested a review from FranzBusch August 18, 2026 14:08

@FranzBusch FranzBusch left a comment

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.

LGTM. One minor nit

metadata: [
"failure-reason": .string(error.reason)
]
metadata: [LoggingKeys.failureReason: .string(error.reason)]

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.

Why don't we pass the error to the log here. Then we can remove that metadata completely

/// `CONNECT_ERROR`, or `0x02` for `INTERNAL_ERROR`.
///
/// This code is used only when the request is served over HTTP/2.
var http2StreamResetCode: UInt32 { get }

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.

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.

Comment thread Sources/NIOHTTPServer/NIOHTTPServer+AbortRequest.swift
Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerStreamResetTests.swift Outdated
Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerStreamResetTests.swift Outdated
Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerStreamResetTests.swift Outdated
Comment thread Tests/NIOHTTPServerTests/NIOHTTPServerStreamResetTests.swift Outdated
@gjcairo
gjcairo requested review from aryan-25 and ehaydenr August 19, 2026 13:41
@FranzBusch
FranzBusch merged commit b7c5b1f into swift-server:main Aug 20, 2026
29 of 43 checks passed
@gjcairo gjcairo mentioned this pull request Aug 20, 2026
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