in Sources/NIOHTTP2/StreamStateMachine.swift [267:395]
mutating func sendHeaders(headers: HPACKHeaders, validateHeaderBlock: Bool, validateContentLength: Bool, isEndStreamSet endStream: Bool) -> StateMachineResultWithStreamEffect {
do {
// We can send headers in the following states:
//
// - idle, when we are a client, in which case we are sending our request headers
// - halfOpenRemoteLocalIdle, in which case we are a server sending either informational or final headers
// - halfOpenLocalPeerIdle, in which case we are a client sending trailers
// - reservedLocal, in which case we are a server sending either informational or final headers
// - fullyOpen, in which case we are sending trailers
// - halfClosedRemoteLocalIdle, in which case we area server sending either informational or final headers
// (see the comment on halfClosedRemoteLocalIdle for more)
// - halfClosedRemoteLocalActive, in which case we are sending trailers
//
// In idle or reservedLocal we are opening the stream. In reservedLocal, halfClosedRemoteLocalIdle, or halfClosedremoteLocalActive
// we may be closing the stream. The keen-eyed may notice that reservedLocal may both open *and* close a stream. This is a bit awkward
// for us, and requires a separate event.
switch self.state {
case .idle(.client, localWindow: let localWindow, remoteWindow: let remoteWindow):
let targetState: State
let localContentLength = validateContentLength ? ContentLengthVerifier(headers) : .disabled
if endStream {
try localContentLength.endOfStream()
targetState = .halfClosedLocalPeerIdle(remoteWindow: remoteWindow)
} else {
targetState = .halfOpenLocalPeerIdle(localWindow: localWindow, localContentLength: localContentLength, remoteWindow: remoteWindow)
}
let targetEffect: StreamStateChange = .streamCreated(.init(streamID: self.streamID, localStreamWindowSize: Int(localWindow), remoteStreamWindowSize: Int(remoteWindow)))
return self.processRequestHeaders(headers,
validateHeaderBlock: validateHeaderBlock,
targetState: targetState,
targetEffect: targetEffect)
case .halfOpenRemoteLocalIdle(localWindow: let localWindow, remoteContentLength: let remoteContentLength, remoteWindow: let remoteWindow):
let targetState: State
let localContentLength = validateContentLength ? ContentLengthVerifier(headers) : .disabled
if endStream {
try localContentLength.endOfStream()
targetState = .halfClosedLocalPeerActive(localRole: .server, initiatedBy: .client, remoteContentLength: remoteContentLength, remoteWindow: remoteWindow)
} else {
targetState = .fullyOpen(localRole: .server, localContentLength: localContentLength, remoteContentLength: remoteContentLength, localWindow: localWindow, remoteWindow: remoteWindow)
}
return self.processResponseHeaders(headers,
validateHeaderBlock: validateHeaderBlock,
targetStateIfFinal: targetState,
targetEffectIfFinal: nil)
case .halfOpenLocalPeerIdle(localWindow: _, localContentLength: let localContentLength, remoteWindow: let remoteWindow):
try localContentLength.endOfStream()
return self.processTrailers(headers,
validateHeaderBlock: validateHeaderBlock,
isEndStreamSet: endStream,
targetState: .halfClosedLocalPeerIdle(remoteWindow: remoteWindow),
targetEffect: nil)
case .reservedLocal(let localWindow):
let targetState: State
let targetEffect: StreamStateChange
let localContentLength = validateContentLength ? ContentLengthVerifier(headers) : .disabled
if endStream {
try localContentLength.endOfStream()
targetState = .closed(reason: nil)
targetEffect = .streamCreatedAndClosed(.init(streamID: self.streamID))
} else {
targetState = .halfClosedRemoteLocalActive(localRole: .server, initiatedBy: .server, localContentLength: localContentLength, localWindow: localWindow)
targetEffect = .streamCreated(.init(streamID: self.streamID, localStreamWindowSize: Int(localWindow), remoteStreamWindowSize: nil))
}
return self.processResponseHeaders(headers,
validateHeaderBlock: validateHeaderBlock,
targetStateIfFinal: targetState,
targetEffectIfFinal: targetEffect)
case .fullyOpen(let localRole, localContentLength: let localContentLength, remoteContentLength: let remoteContentLength, localWindow: _, remoteWindow: let remoteWindow):
try localContentLength.endOfStream()
return self.processTrailers(headers,
validateHeaderBlock: validateHeaderBlock,
isEndStreamSet: endStream,
targetState: .halfClosedLocalPeerActive(localRole: localRole, initiatedBy: .client, remoteContentLength: remoteContentLength, remoteWindow: remoteWindow),
targetEffect: nil)
case .halfClosedRemoteLocalIdle(let localWindow):
let targetState: State
let targetEffect: StreamStateChange?
let localContentLength = validateContentLength ? ContentLengthVerifier(headers) : .disabled
if endStream {
try localContentLength.endOfStream()
targetState = .closed(reason: nil)
targetEffect = .streamClosed(.init(streamID: self.streamID, reason: nil))
} else {
targetState = .halfClosedRemoteLocalActive(localRole: .server, initiatedBy: .client, localContentLength: localContentLength, localWindow: localWindow)
targetEffect = nil
}
return self.processResponseHeaders(headers,
validateHeaderBlock: validateHeaderBlock,
targetStateIfFinal: targetState,
targetEffectIfFinal: targetEffect)
case .halfClosedRemoteLocalActive(localRole: _, initiatedBy: _, localContentLength: let localContentLength, localWindow: _):
try localContentLength.endOfStream()
return self.processTrailers(headers,
validateHeaderBlock: validateHeaderBlock,
isEndStreamSet: endStream,
targetState: .closed(reason: nil),
targetEffect: .streamClosed(.init(streamID: self.streamID, reason: nil)))
// Sending a HEADERS frame as an idle server, or on a closed stream, is a connection error
// of type PROTOCOL_ERROR. In any other state, sending a HEADERS frame is a stream error of
// type PROTOCOL_ERROR.
// (Authors note: I can find nothing in the RFC that actually states what kind of error is
// triggered for HEADERS frames outside the valid states. So I just guessed here based on what
// seems reasonable to me: specifically, if we have a stream to fail, fail it, otherwise treat
// the error as connection scoped.)
case .idle(.server, _, _), .closed:
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.badStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
case .reservedRemote, .halfClosedLocalPeerIdle, .halfClosedLocalPeerActive:
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.badStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
}
} catch let error where error is NIOHTTP2Errors.ContentLengthViolated {
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .protocolError), effect: nil)
} catch {
preconditionFailure("Unexpected error: \(error)")
}
}