Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 15 additions & 0 deletions Sources/HTTP3/HTTP3Frame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,21 @@ extension HTTP3PartialFrame {
}
}

extension HTTP3Frame.Headers {
/// Whether these headers contain a `:status` pseudo-header field whose value is an informational 1xx status code.
///
/// Returns `false` if the `:status` pseudo-header field is not present or if its value does not represent a 1xx
/// status code.
///
/// - SeeAlso: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.2.
package var representsInterimResponse: Bool {
Comment thread
aryan-25 marked this conversation as resolved.
guard let status = self.fields.first(where: { $0.name == .status })?.value, let code = Int(status) else {
return false
}
return (100..<200).contains(code)
}
}

extension HTTP3Frame {
package static func data(_ payload: ByteBuffer) -> Self {
.data(.init(payload: payload))
Expand Down
3 changes: 1 addition & 2 deletions Sources/HTTP3/HTTP3FrameValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ package enum HTTP3FrameValidator: ~Copyable {
case .headers(let headers):
// A response MAY consist of multiple messages when and only when one or more interim responses (1xx; see Section 15.2 of [HTTP]) precede a final response to the same request.
// Interim responses do not contain content or trailer sections.
let status = headers.fields.first(where: { $0.name == .status })?.value
if status == "100" {
if headers.representsInterimResponse {
// This header doesn't affect our state because it's interim. We just pass it through
self = .init(requestState: existingRequestState, responseState: existingResponseState)
return .forwardFrame(frame)
Expand Down
17 changes: 12 additions & 5 deletions Sources/NIOHTTP3/HTTP3ToHTTPCodecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,13 @@ extension HTTPResponsePart: HTTPMessagePart {
/// Use this to convert incoming frames into message parts.
package struct HTTPMessageParsingStateMachine<Part: HTTPMessagePart> {
enum State {
case idle
case awaitingFinalHead
case processedHeaders
case processedTrailers
case previousError
}

private var state = State.idle
private var state = State.awaitingFinalHead

package init() {}

Expand All @@ -215,12 +215,19 @@ package struct HTTPMessageParsingStateMachine<Part: HTTPMessagePart> {
switch self.state {
case .previousError:
return .none
case .idle:
case .awaitingFinalHead:
switch frame {
case .headers(let headers):
do {
let part = try Part.head(fields: headers.fields)
self.state = .processedHeaders
if headers.representsInterimResponse {
// Multiple interim (1xx) responses can precede the final response; remain in the same state to
// accept further interim responses or the final response. We can only reach this branch on the
// response parsing side.
self.state = .awaitingFinalHead
} else {
self.state = .processedHeaders
}
return .returnPart(part)
} catch {
self.state = .previousError
Expand Down Expand Up @@ -261,7 +268,7 @@ package struct HTTPMessageParsingStateMachine<Part: HTTPMessagePart> {

package mutating func inputClosed() -> InputClosedAction? {
switch self.state {
case .idle:
case .awaitingFinalHead:
self.state = .processedTrailers
return .returnPart(.end())
case .processedHeaders:
Expand Down
41 changes: 40 additions & 1 deletion Tests/HTTP3Tests/HTTP3FrameValidatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,19 +306,58 @@ struct HTTP3FrameValidatorTests {
}

@Test
func testDoubleResponse() {
func testInboundInterimResponse() {
var validator = HTTP3FrameValidator(streamType: .request, incoming: false)
validator.assertOutboundFramePassesThrough(Self.validRequestHeaders())

validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "103")]))
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "200")]))
validator.assertInboundFramePassesThrough(.data(.init(bytes: [1, 2, 3])))
validator.assertInboundFramePassesThrough(Self.validTrailers())
}

@Test
func testInboundMultipleInterimResponse() {
var validator = HTTP3FrameValidator(streamType: .request, incoming: false)
validator.assertOutboundFramePassesThrough(Self.validRequestHeaders()) // write req headers

// Some informational responses
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "100")]))
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "100")]))
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "102")]))
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "103")]))
// Final response
validator.assertInboundFramePassesThrough(.headers([.init(name: .status, value: "200")]))
validator.assertInboundFramePassesThrough(.data(.init(bytes: [1, 2, 3])))
validator.assertInboundFramePassesThrough(.headers([.init(name: .cookie, value: "test")]))
}

@Test
func testOutboundInterimResponse() {
var validator = HTTP3FrameValidator(streamType: .request, incoming: true)
validator.assertInboundFramePassesThrough(Self.validRequestHeaders())

validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "103")]))
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "200")]))
validator.assertOutboundFramePassesThrough(.data(.init(bytes: [1, 2, 3])))
}

@Test
func testOutboundMultipleInterimResponses() {
var validator = HTTP3FrameValidator(streamType: .request, incoming: true)
validator.assertInboundFramePassesThrough(Self.validRequestHeaders())

// Some informational responses
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "100")]))
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "100")]))
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "102")]))
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "103")]))
// Final response
validator.assertOutboundFramePassesThrough(.headers([.init(name: .status, value: "200")]))
validator.assertOutboundFramePassesThrough(.data(.init(bytes: [1, 2, 3])))
validator.assertOutboundFramePassesThrough(.headers([.init(name: .cookie, value: "test")]))
}

// MARK: Control stream

/// Anything other than settings is invalid to be the first frame.
Expand Down
64 changes: 64 additions & 0 deletions Tests/NIOHTTP3Tests/HTTPMessageParsingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,70 @@ struct HTTPMessageParsingTests {
#expect(action3 == nil)
}

let informationalEarlyHintsHead = HTTP3Frame.headers([.init(name: .status, value: "103")])

@Test
func testSingleInterimThenFinal() throws {
var machine = HTTPMessageParsingStateMachine<HTTPResponsePart>()

let action1 = machine.processFrame(frame: self.informationalEarlyHintsHead)
#expect(action1?.returnPart == .head(.init(status: .init(code: 103))))

let action2 = machine.processFrame(frame: self.validResponseHead)
#expect(action2?.returnPart == .head(.init(status: .ok)))

let action3 = machine.processFrame(frame: .data(.init()))
#expect(action3?.returnPart == .body(.init()))

let action4 = machine.processFrame(frame: self.validTrailers)
#expect(action4?.returnPart == .end([.init("test")!: "hello"]))
}

@Test
func testMultipleInterimThenFinal() throws {
var machine = HTTPMessageParsingStateMachine<HTTPResponsePart>()

let action1 = machine.processFrame(frame: self.informationalEarlyHintsHead)
#expect(action1?.returnPart == .head(.init(status: .init(code: 103))))

let action2 = machine.processFrame(frame: self.informationalEarlyHintsHead)
#expect(action2?.returnPart == .head(.init(status: .init(code: 103))))

let action3 = machine.processFrame(frame: self.validResponseHead)
#expect(action3?.returnPart == .head(.init(status: .ok)))

let action4 = machine.processFrame(frame: .data(.init()))
#expect(action4?.returnPart == .body(.init()))

let action5 = machine.processFrame(frame: self.validTrailers)
#expect(action5?.returnPart == .end([.init("test")!: "hello"]))
}

@Test
func testInvalidInterimHeaders() throws {
var machine = HTTPMessageParsingStateMachine<HTTPResponsePart>()

// 1xx response with a forbidden transfer-encoding field.
let action1 = machine.processFrame(
frame: .headers([
.init(name: .status, value: "103"),
.init(name: .transferEncoding, value: "chunked"),
])
)
action1.assertError { error in
expectH3ErrorEqual(
error: error,
expectedCode: .malformedMessage,
expectedH3ErrorCode: .H3_MESSAGE_ERROR,
expectedMessage: "Invalid headers"
)
}

// After error, even valid final headers are ignored.
let action2 = machine.processFrame(frame: self.validResponseHead)
#expect(action2 == nil)
}

// MARK: General request and response header validation

@Test
Expand Down
Loading