Skip to content

Commit 5468df9

Browse files
authored
Correctly handle inputClosed event (#13)
### Motivation In `HTTP3ToHTTPServerCodec` and `HTTP3ToHTTPClientCodec`, we currently emit a response/request end part if the client/server terminated the stream with an incomplete response/request. This means that downstream handlers can receive a response/request end part before receiving a head/body part. Per [RFC 9114 §4.1](https://datatracker.ietf.org/doc/html/rfc9114#section-4.1-14), servers should abort the response stream with the `H3_INCOMPLETE_REQUEST` error code when a client-initiated stream is terminated before receiving enough request parts to provide a full response. Additionally, [§4.1.1](https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.1-6) describes that clients should not use partial responses. ### Modifications - When we receive the `.inputClosed` event, `HTTP3FrameValidator` now checks whether it received a complete request/response, and returns one of three actions: `.doNothing` (if we received a complete request/response), `.notifyDownstream` (if we received an incomplete response (clients only)), or `.resetStream` (if we received an incomplete request (servers only)). - `HTTP3StreamStateMachine` now passes that action as an associated value on the `inputClosed` action. - `HTTP3StreamHandler` fires the `inputClosed` event as before, but for an incomplete message it first fires an `HTTP3Error` down the pipeline, and for servers, it also sends a `QUICResetStreamEvent` with the `H3_REQUEST_INCOMPLETE` code. - `HTTPMessageParsingStateMachine` no longer emits an end part when the input closes before a head part arrives. - Added a `peerTerminatedInboundStream` case to `HTTP3Error.Code`. ### Result `HTTP3ToHTTPServerCodec` and `HTTP3ToHTTPClientCodec` no longer emit a request/response end part if the stream has terminated without a complete request/response. Instead, `HTTP3StreamHandler` handler notifies downstream handlers with an error, and for servers, the stream is also reset.
1 parent 6cb7c32 commit 5468df9

9 files changed

Lines changed: 682 additions & 38 deletions

Sources/HTTP3/HTTP3Error.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ extension HTTP3Error {
144144
case none
145145
case invalidGoawayStreamID
146146
case criticalStreamClosed
147+
case peerTerminatedInboundStream
147148
}
148149

149150
public var description: String {
@@ -254,6 +255,11 @@ extension HTTP3Error {
254255
public static var criticalStreamClosed: Self {
255256
Self(.criticalStreamClosed)
256257
}
258+
259+
/// The peer terminated the inbound stream before delivering a complete request or response.
260+
public static var peerTerminatedInboundStream: Self {
261+
Self(.peerTerminatedInboundStream)
262+
}
257263
}
258264

259265
/// A location within source code.

Sources/HTTP3/HTTP3FrameValidator.swift

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ package enum HTTP3FrameValidator: ~Copyable {
176176
/// It looks at both request and response frames.
177177
/// When using this validator for a client, the request frames are the outbound and the response frames are inbound.
178178
/// This is reversed for a server.
179-
package struct RequestStreamValidator: ~Copyable {
179+
private struct RequestStreamValidator: ~Copyable {
180180
/// Models the state of one side of the connection.
181181
enum State {
182182
/// Nothing has happened yet.
@@ -357,30 +357,151 @@ package enum HTTP3FrameValidator: ~Copyable {
357357
}
358358
}
359359
}
360+
361+
/// Whether the complete request has been received.
362+
private var receivedCompleteRequest: Bool {
363+
switch self.requestState {
364+
case .idle:
365+
// We haven't received the request head part yet, so the request is trivially incomplete.
366+
return false
367+
368+
case .headersProcessed:
369+
// TODO: If a Content-Length header was specified, we need to check whether we have received all the
370+
// specified bytes. If that is not the case, then the request is malformed per RFC 9114 § 4.1.2, and it
371+
// must be treated as a stream error.
372+
return true
373+
374+
case .trailersProcessed:
375+
// If we have processed trailers, we have seen the full request.
376+
return true
377+
378+
case .previousError:
379+
// There was already an error. That will result in the stream closing anyway, so just return `true`.
380+
return true
381+
}
382+
}
383+
384+
/// Whether the complete response has been received.
385+
private var receivedCompleteResponse: Bool {
386+
switch self.responseState {
387+
case .idle:
388+
// We haven't received a final response head part yet, so the response is trivially incomplete.
389+
return false
390+
391+
case .headersProcessed:
392+
// TODO: If a Content-Length header was specified, we need to check whether we have received all the
393+
// specified bytes. If that is not the case, then the request is malformed per RFC 9114 § 4.1.2, and it
394+
// must be treated as a stream error.
395+
return true
396+
397+
case .trailersProcessed:
398+
// If we have processed trailers, we have seen the full response.
399+
return true
400+
401+
case .previousError:
402+
// There was already an error. That will result in the stream closing anyway, so just return `true`.
403+
return true
404+
}
405+
}
406+
407+
/// Records that the inbound request stream has closed.
408+
mutating func processInboundRequestStreamClosed() -> InboundClosedAction {
409+
if self.receivedCompleteRequest {
410+
return .doNothing
411+
}
412+
413+
// The input closed before we saw a *complete* request. Per RFC 9114 § 4.1:
414+
// "If a client-initiated stream terminates without enough of the HTTP message to provide a complete
415+
// response, the server SHOULD abort its response stream with the error code H3\_REQUEST\_INCOMPLETE".
416+
//
417+
// We therefore need to reset the stream.
418+
self = .init(requestState: .previousError, responseState: .previousError)
419+
420+
return .resetStream(
421+
HTTP3Error(
422+
code: .peerTerminatedInboundStream,
423+
message: "Inbound request stream closed before a complete request was received",
424+
cause: nil,
425+
errorCode: .requestIncomplete,
426+
location: .here()
427+
)
428+
)
429+
}
430+
431+
/// Records that the inbound response stream has closed.
432+
func processInboundResponseStreamClosed() -> InboundClosedAction {
433+
if self.receivedCompleteResponse {
434+
return .doNothing
435+
}
436+
437+
// The input closed before we saw a *complete* response. Per RFC 9114 § 4.1.1:
438+
// "... if a stream is cancelled after receiving a partial response, the response SHOULD NOT be used".
439+
//
440+
// We therefore need to inform the downstream so they can decide what to do with the incomplete response.
441+
//
442+
// This is not a stream or connection error, so we don't modify the request or response state.
443+
return .notifyDownstream(
444+
HTTP3Error(
445+
code: .peerTerminatedInboundStream,
446+
message: "Inbound response stream closed before a complete response was received",
447+
cause: nil,
448+
errorCode: nil,
449+
location: .here()
450+
)
451+
)
452+
}
453+
}
454+
455+
/// The action to take when the inbound side of a stream closes.
456+
package enum InboundClosedAction {
457+
/// We received the full request or response before the inbound closed. As such, there is nothing to do.
458+
case doNothing
459+
460+
/// The inbound closed before we received the full response. The downstream should be notified so they can
461+
/// decide what to do with the partial response.
462+
case notifyDownstream(HTTP3Error)
463+
464+
/// The inbound closed before we received the full request. Per RFC 9114 § 4.1, the server should abort the
465+
/// response stream by sending a RESET_STREAM frame.
466+
case resetStream(HTTP3Error)
360467
}
361468

362469
package struct ServerRequestStreamValidator: ~Copyable {
363470
private var underlying = RequestStreamValidator()
364471

472+
/// Validates an inbound request frame.
365473
package mutating func processInboundFrame(_ frame: HTTP3Frame) -> ProcessFrameAction {
366474
self.underlying.processRequestFrame(frame)
367475
}
368476

477+
/// Validates an outbound response frame.
369478
package mutating func processOutboundFrame(_ frame: HTTP3Frame) -> ProcessFrameAction {
370479
self.underlying.processResponseFrame(frame)
371480
}
481+
482+
/// Records that the inbound request stream has closed.
483+
package mutating func processInboundClosed() -> InboundClosedAction {
484+
self.underlying.processInboundRequestStreamClosed()
485+
}
372486
}
373487

374488
package struct ClientRequestStreamValidator: ~Copyable {
375489
private var underlying = RequestStreamValidator()
376490

491+
/// Validates an inbound response frame.
377492
package mutating func processInboundFrame(_ frame: HTTP3Frame) -> ProcessFrameAction {
378493
self.underlying.processResponseFrame(frame)
379494
}
380495

496+
/// Validates an outbound request frame.
381497
package mutating func processOutboundFrame(_ frame: HTTP3Frame) -> ProcessFrameAction {
382498
self.underlying.processRequestFrame(frame)
383499
}
500+
501+
/// Records that the inbound response stream has closed.
502+
package func processInboundClosed() -> InboundClosedAction {
503+
self.underlying.processInboundResponseStreamClosed()
504+
}
384505
}
385506

386507
/// Checks that incoming frames are in the right order for a control stream, and that there are no outgoing frames.
@@ -594,6 +715,37 @@ package enum HTTP3FrameValidator: ~Copyable {
594715
return .dropFrame
595716
}
596717
}
718+
719+
/// Records that the inbound side of the stream has closed.
720+
package mutating func processInboundClosed() -> InboundClosedAction {
721+
switch self {
722+
case .incomingRequestStream(var validator):
723+
let result = validator.processInboundClosed()
724+
self = .incomingRequestStream(validator)
725+
return result
726+
727+
case .outgoingRequestStream(let validator):
728+
let result = validator.processInboundClosed()
729+
self = .outgoingRequestStream(validator)
730+
return result
731+
732+
case .incomingControlStream(let validator):
733+
self = .incomingControlStream(validator)
734+
return .doNothing
735+
736+
case .outgoingControlStream(let validator):
737+
self = .outgoingControlStream(validator)
738+
return .doNothing
739+
740+
case .incomingPushStream(let validator):
741+
self = .incomingPushStream(validator)
742+
return .doNothing
743+
744+
case .outgoingPushStream(let validator):
745+
self = .outgoingPushStream(validator)
746+
return .doNothing
747+
}
748+
}
597749
}
598750

599751
extension HTTP3Frame {

0 commit comments

Comments
 (0)