-
Notifications
You must be signed in to change notification settings - Fork 11
Reset streams when throwing from request handler #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
492d385
Reset streams when throwing from request handler
gjcairo 6a7a2d0
Logging changes
gjcairo 43df3fd
Make HTTP version enum nonexhaustive
gjcairo 7fe93df
PR changes
gjcairo 452529d
PR changes
gjcairo 461ea6e
Format
gjcairo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
Sources/NIOHTTPServer/HTTPServerHTTP2StreamResetErrorConvertible.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| /// An error that maps to the `RST_STREAM` error code sent when an HTTP/2 request is aborted. | ||
| /// | ||
| /// A request handler reports a failure by throwing. The server does not surface that error to any caller: instead it | ||
| /// aborts the exchange on the wire, which over HTTP/2 means resetting the request's stream. Conform an error to this | ||
| /// protocol to choose the error code carried by that `RST_STREAM` frame. | ||
| /// | ||
| /// An error that does not conform is reset with `INTERNAL_ERROR` (`0x02`). | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// A proxy that fails to establish a tunnel reports it as a `CONNECT` error: | ||
| /// | ||
| /// ```swift | ||
| /// struct TunnelFailure: HTTPServerHTTP2StreamResetErrorConvertible { | ||
| /// var http2StreamResetCode: UInt32 { 0x0a } // CONNECT_ERROR | ||
| /// } | ||
| /// | ||
| /// try await server.serve { request, context, reader, responseSender in | ||
| /// guard let tunnel = try? await openTunnel(to: request.authority) else { | ||
| /// throw TunnelFailure() | ||
| /// } | ||
| /// // ... | ||
| /// } | ||
| /// ``` | ||
| public protocol HTTPServerHTTP2StreamResetErrorConvertible: Error { | ||
| /// The `RST_STREAM` error code to send, as its numeric value on the wire. | ||
| /// | ||
| /// The codes and their values are defined by RFC 9113 § 7 — for example `0x08` for `CANCEL`, `0x0a` for | ||
| /// `CONNECT_ERROR`, or `0x02` for `INTERNAL_ERROR`. | ||
| /// | ||
| /// This code is used only when the request is served over HTTP/2. | ||
| var http2StreamResetCode: UInt32 { get } | ||
| } | ||
57 changes: 57 additions & 0 deletions
57
Sources/NIOHTTPServer/HTTPServerHTTP3StreamResetErrorConvertible.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
| /// An error that maps to the error codes sent when an HTTP/3 request is aborted. | ||
| /// | ||
| /// A request handler reports a failure by throwing. The server does not surface that error to any caller: instead it | ||
| /// aborts the exchange on the wire, which over HTTP/3 means resetting the request's stream and asking the client to | ||
| /// stop sending the request body. Conform an error to this protocol to choose the error codes carried by those frames. | ||
| /// | ||
| /// An error that does not conform is reset with `H3_INTERNAL_ERROR` (`0x0102`). | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// A proxy that fails to establish a tunnel reports it as a `CONNECT` error: | ||
| /// | ||
| /// ```swift | ||
| /// struct TunnelFailure: HTTPServerHTTP3StreamResetErrorConvertible { | ||
| /// var http3StreamResetCode: UInt64 { 0x010f } // H3_CONNECT_ERROR | ||
| /// var http3StopSendingCode: UInt64 { 0x010f } // H3_CONNECT_ERROR | ||
| /// } | ||
| /// | ||
| /// try await server.serve { request, context, reader, responseSender in | ||
| /// guard let tunnel = try? await openTunnel(to: request.authority) else { | ||
| /// throw TunnelFailure() | ||
| /// } | ||
| /// // ... | ||
| /// } | ||
| /// ``` | ||
| public protocol HTTPServerHTTP3StreamResetErrorConvertible: Error { | ||
| /// The application error code to send when abandoning the response, as its numeric value on the wire. | ||
| /// | ||
| /// The codes and their values are defined by RFC 9114 § 8.1 — for example `0x010f` for `H3_CONNECT_ERROR`, | ||
| /// `0x010c` for `H3_REQUEST_CANCELLED`, or `0x0102` for `H3_INTERNAL_ERROR`. | ||
| /// | ||
| /// The value must be less than 2^62, the largest value the transport can encode; an out-of-range value is replaced | ||
| /// with `H3_INTERNAL_ERROR`. This code is used only when the request is served over HTTP/3. | ||
| var http3StreamResetCode: UInt64 { get } | ||
|
|
||
| /// The application error code to send when asking the client to stop sending the request body that the server is no | ||
| /// longer reading, as its numeric value on the wire. | ||
| /// | ||
| /// The same code space and range restriction as ``http3StreamResetCode`` applies. | ||
| var http3StopSendingCode: UInt64 { get } | ||
| } | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| enum LoggingKeys { | ||
| static var `protocol`: String { "protocol" } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
| import NIOHTTP2 | ||
| import NIOHTTPTypes | ||
| import NIOHTTPTypesHTTP2 | ||
|
|
||
| #if HTTP3 | ||
| import HTTP3 | ||
| import NIOQUICHelpers | ||
| #endif | ||
|
|
||
| @available(anyAppleOS 26.0, *) | ||
| extension NIOHTTPServer { | ||
| /// Aborts the exchange carrying a request on the wire, after that request's handler threw `error`. | ||
| /// | ||
| /// Which mechanism applies depends on the protocol serving the request: | ||
| /// - HTTP/1.1 has no stream to reset, so the response is abandoned and the connection is closed: a decision delegated to | ||
| /// ``HTTPKeepAliveHandler``, which already tracks how far the response has progressed and owns the | ||
| /// `Connection: close` handling. | ||
| /// - HTTP/2 and HTTP/3 reset the request's own stream, with the error codes `error` describes. | ||
| /// - HTTP/3 also asks the client to STOP_SENDING. | ||
| static func abortRequest(requestContext: RequestContext, error: any Error) { | ||
| let channel = requestContext.channel | ||
|
|
||
| switch requestContext.connectionContext.httpVersion { | ||
| case .plaintextHTTP1_1, .http1_1: | ||
| var response = HTTPResponse(status: .internalServerError) | ||
| response.headerFields[.contentLength] = "0" | ||
|
aryan-25 marked this conversation as resolved.
|
||
| channel.triggerUserOutboundEvent( | ||
| HTTPKeepAliveHandler.RequestAborted(responseIfNotStarted: response), | ||
| promise: nil | ||
| ) | ||
|
|
||
| case .http2: | ||
| // An error that does not describe its own code is reset with `INTERNAL_ERROR`. | ||
| let resetCode = | ||
| (error as? any HTTPServerHTTP2StreamResetErrorConvertible) | ||
| .map { HTTP2ErrorCode(networkCode: Int($0.http2StreamResetCode)) } ?? .internalError | ||
|
|
||
| // `HTTP2FramePayloadToHTTPServerCodec` translates this event into a `RST_STREAM` frame. | ||
| channel.triggerUserOutboundEvent( | ||
| NIOHTTP2FramePayloadToHTTPEvent.reset(code: resetCode), | ||
| promise: nil | ||
| ) | ||
|
|
||
| #if HTTP3 | ||
| case .http3: | ||
| let http3Error = error as? any HTTPServerHTTP3StreamResetErrorConvertible | ||
| let resetCode = Self.quicErrorCode(http3Error?.http3StreamResetCode) | ||
| let stopSendingCode = Self.quicErrorCode(http3Error?.http3StopSendingCode) | ||
|
|
||
| // `RESET_STREAM` abandons the response direction and `STOP_SENDING` asks the client to | ||
| // stop sending the request body. | ||
| channel.triggerUserOutboundEvent(QUICResetStreamEvent(code: resetCode), promise: nil) | ||
| channel.triggerUserOutboundEvent(QUICStopSendingEvent(code: stopSendingCode), promise: nil) | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| #if HTTP3 | ||
| /// Converts a raw HTTP/3 error code into a QUIC application error code. | ||
| /// | ||
| /// Substitutes `H3_INTERNAL_ERROR` when the error described no code, or described one that cannot be represented as | ||
| /// a QUIC variable-length integer. | ||
| private static func quicErrorCode(_ rawValue: UInt64?) -> QUICApplicationErrorCode { | ||
| // The force unwrap is safe: `H3_INTERNAL_ERROR` (0x0102) is always representable as a QUIC varint. | ||
| rawValue.flatMap(QUICApplicationErrorCode.init) | ||
| ?? QUICApplicationErrorCode(HTTP3ErrorCode.internalError.rawValue)! | ||
| } | ||
| #endif | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,8 @@ public import X509 | |
| @available(anyAppleOS 26.0, *) | ||
| extension NIOHTTPServer { | ||
| /// The application-level HTTP version negotiated for a connection. | ||
| public enum HTTPVersion: String, Sendable, Hashable { | ||
| @nonexhaustive | ||
| public enum HTTPVersion: String, Sendable, Hashable, CaseIterable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to mark this as |
||
| case plaintextHTTP1_1 = "Plaintext HTTP/1.1" | ||
| case http1_1 = "HTTP/1.1" | ||
| case http2 = "HTTP/2" | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you consider using https://github.com/apple/swift-nio-http2/blob/281170341aeff301a62dab0ba13f9829ff148989/Sources/NIOHTTP2/HTTP2ErrorCode.swift#L21 instead of a raw UInt32?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #115 (comment)