Skip to content

Commit 91fb4bb

Browse files
committed
Move handling of inputClosed to HTTP3StreamHandler
1 parent c09c0d7 commit 91fb4bb

9 files changed

Lines changed: 607 additions & 132 deletions

Sources/HTTP3/HTTP3Error.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ extension HTTP3Error {
140140
case none
141141
case invalidGoawayStreamID
142142
case criticalStreamClosed
143-
case peerTerminatedStream
143+
case peerTerminatedInboundStream
144144
}
145145

146146
public var description: String {
@@ -252,9 +252,9 @@ extension HTTP3Error {
252252
Self(.criticalStreamClosed)
253253
}
254254

255-
/// The peer terminated the stream before delivering a complete request or response.
256-
public static var peerTerminatedStream: Self {
257-
Self(.peerTerminatedStream)
255+
/// The peer terminated the inbound stream before delivering a complete request or response.
256+
public static var peerTerminatedInboundStream: Self {
257+
Self(.peerTerminatedInboundStream)
258258
}
259259
}
260260

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: .H3_REQUEST_INCOMPLETE,
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: .H3_NO_ERROR,
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 {

Sources/HTTP3/HTTP3StreamStateMachine.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ package struct HTTP3StreamStateMachine: ~Copyable {
195195
}
196196
}
197197

198-
/// Inform the state machine of a qpack decode result that has been previously been asked for.
198+
/// Inform the state machine of a qpack decode result that has been previously asked for.
199199
/// It is an error to call this function with a result for a partial header which wasn't asked for.
200200
mutating func gotHeaderDecodeResult(_ decoded: [HTTPField], from: HTTP3PartialFrame.Headers) {
201201
switch consume self.state {
@@ -577,7 +577,7 @@ package struct HTTP3StreamStateMachine: ~Copyable {
577577
/// A frame is ready, but you need to decode it and call the state machine back with the result.
578578
case decodeHeader(HTTP3PartialFrame.Headers)
579579
/// The input was newly closed.
580-
case inputClosed
580+
case inputClosed(InputClosedAction)
581581
/// The input was already closed
582582
case alreadyClosed
583583
/// More input is needed before the next action can be determined
@@ -586,6 +586,19 @@ package struct HTTP3StreamStateMachine: ~Copyable {
586586
case previousError
587587
/// The decodeNext() function should be called again to get the next action.
588588
case callAgain
589+
590+
package enum InputClosedAction {
591+
/// A complete request/response was received before the input was closed. As such, we should just deliver
592+
/// the `inputClosed` event downstream.
593+
case emitEvent
594+
595+
/// A complete response was not received before the input was closed. We need to notify the downstream about
596+
/// the incompleteness through an error and then deliver the `inputClosed` event.
597+
case emitErrorAndEvent(HTTP3Error)
598+
599+
/// A complete request was not received before the input was closed. We need to send a RESET\_STREAM frame.
600+
case resetStream(HTTP3Error)
601+
}
589602
}
590603

591604
/// Read out the next frame if it is ready. This may ask you to run qpack on some partial headers.
@@ -643,8 +656,20 @@ package struct HTTP3StreamStateMachine: ~Copyable {
643656
self = .init(state: .idle(idleState))
644657
return .needMoreBytes
645658
case .inputClosed:
646-
self = .init(state: .idle(idleState))
647-
return .inputClosed
659+
// The input was closed. Inform the validator to determine what to do next.
660+
switch idleState.validator.processInboundClosed() {
661+
case .doNothing:
662+
self = .init(state: .idle(idleState))
663+
return .inputClosed(.emitEvent)
664+
665+
case .notifyDownstream(let error):
666+
self = .init(state: .idle(idleState))
667+
return .inputClosed(.emitErrorAndEvent(error))
668+
669+
case .resetStream(let error):
670+
self = .init(state: .idle(idleState))
671+
return .inputClosed(.resetStream(error))
672+
}
648673
}
649674
case .finished:
650675
self = .init(state: .finished)

Sources/NIOHTTP3/HTTP3StreamHandler.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ package final class HTTP3StreamHandler: ChannelDuplexHandler {
116116
var didFireChannelRead = false
117117
loop: for action in actionBuffer {
118118
switch action {
119-
case .inputClosed:
120-
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
119+
case .inputClosed(let inputClosedAction):
120+
self.handleInputClosed(action: inputClosedAction, context: context)
121121
case .returnFrame(let frame):
122122
context.fireChannelRead(wrapInboundOut(frame))
123123
didFireChannelRead = true
@@ -167,8 +167,8 @@ package final class HTTP3StreamHandler: ChannelDuplexHandler {
167167
decodeLoop: while true {
168168
let action = self.stateMachine.decodeNext()
169169
switch action {
170-
case .inputClosed:
171-
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
170+
case .inputClosed(let inputClosedAction):
171+
self.handleInputClosed(action: inputClosedAction, context: context)
172172
case .needMoreBytes, .alreadyClosed, .previousError:
173173
break decodeLoop
174174
case .callAgain:
@@ -282,6 +282,29 @@ package final class HTTP3StreamHandler: ChannelDuplexHandler {
282282
}
283283
}
284284

285+
/// Processes the `InputClosedAction`.
286+
private func handleInputClosed(
287+
action: HTTP3StreamStateMachine.DecodeNextAction.InputClosedAction,
288+
context: ChannelHandlerContext
289+
) {
290+
switch action {
291+
case .emitEvent:
292+
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
293+
294+
case .emitErrorAndEvent(let error):
295+
context.fireErrorCaught(error)
296+
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
297+
298+
case .resetStream(let error):
299+
context.triggerUserOutboundEvent(
300+
QUICResetStreamEvent(code: QUICApplicationErrorCode(error.h3ErrorCode ?? .H3_NO_ERROR)),
301+
promise: nil
302+
)
303+
context.fireErrorCaught(error)
304+
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
305+
}
306+
}
307+
285308
package func errorCaught(context: ChannelHandlerContext, error: any Error) {
286309
switch error {
287310
case let error as QUICStreamResetError:
@@ -311,7 +334,7 @@ package final class HTTP3StreamHandler: ChannelDuplexHandler {
311334
package func onQPACKDecodeResult(fields: [HTTPField], forHeaders headers: HTTP3PartialFrame.Headers) {
312335
self.logger.trace("HTTP3StreamHandler.onQPACKDecodeResult")
313336
guard let context = self.context else {
314-
// The stream must have been created an registered to get QPACK events and thus already have
337+
// The stream must have been created and registered to get QPACK events and thus already have
315338
// the context available. Since pending decodes are dropped when the stream closes it must
316339
// still be open and active.
317340
fatalError("Tried to deliver QPACK results before handler was added")

0 commit comments

Comments
 (0)