Skip to content

Commit 859f6b4

Browse files
blueeorLukasa
authored andcommitted
Issue #151 Update BadStreamStateTransition to have stream state (#175)
Update BadStreamStateTransition to have an enum NIOHTTP2StreamState that shows which state the stream was in before the error.
1 parent a8f89aa commit 859f6b4

File tree

4 files changed

+166
-26
lines changed

4 files changed

+166
-26
lines changed

Sources/NIOHTTP2/HTTP2Error.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,21 @@ public enum NIOHTTP2Errors {
6060
}
6161

6262
/// A stream state transition was attempted that was not valid.
63-
public struct BadStreamStateTransition: NIOHTTP2Error {
64-
public init() { }
63+
public struct BadStreamStateTransition: NIOHTTP2Error, CustomStringConvertible {
64+
public let fromState: NIOHTTP2StreamState?
65+
66+
public var description: String {
67+
let stateName = fromState != nil ? "\(fromState!)" : "unknown state"
68+
return "BadStreamStateTransition in state \(stateName)"
69+
}
70+
71+
init(from state: NIOHTTP2StreamState) {
72+
fromState = state
73+
}
74+
75+
public init() {
76+
self.fromState = nil
77+
}
6578
}
6679

6780
/// An attempt was made to change the flow control window size, either via

Sources/NIOHTTP2/StreamStateMachine.swift

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import NIOHPACK
9696
/// Operations on the state machine are performed by calling specific functions corresponding to
9797
/// the operation that is about to occur.
9898
struct HTTP2StreamStateMachine {
99-
private enum State {
99+
fileprivate enum State {
100100
// TODO(cory): Can we remove the idle state? Streams shouldn't sit in idle for long periods
101101
// of time, they should immediately transition out, so can we avoid it entirely?
102102
/// In the idle state, the stream has not been opened by either peer.
@@ -191,7 +191,6 @@ struct HTTP2StreamStateMachine {
191191
case closed(reason: HTTP2ErrorCode?)
192192
}
193193

194-
195194
/// The possible roles an endpoint may play in a given stream.
196195
enum StreamRole {
197196
/// A server. Servers initiate streams by pushing them.
@@ -384,9 +383,9 @@ extension HTTP2StreamStateMachine {
384383
// seems reasonable to me: specifically, if we have a stream to fail, fail it, otherwise treat
385384
// the error as connection scoped.)
386385
case .idle(.server, _, _), .closed:
387-
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
386+
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
388387
case .reservedRemote, .halfClosedLocalPeerIdle, .halfClosedLocalPeerActive:
389-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
388+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
390389
}
391390
} catch let error where error is NIOHTTP2Errors.ContentLengthViolated {
392391
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .protocolError), effect: nil)
@@ -515,9 +514,9 @@ extension HTTP2StreamStateMachine {
515514
// seems reasonable to me: specifically, if we have a stream to fail, fail it, otherwise treat
516515
// the error as connection scoped.)
517516
case .idle(.client, _, _), .closed:
518-
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
517+
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
519518
case .reservedLocal, .halfClosedRemoteLocalIdle, .halfClosedRemoteLocalActive:
520-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
519+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
521520
}
522521
} catch let error where error is NIOHTTP2Errors.ContentLengthViolated {
523522
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .protocolError), effect: nil)
@@ -587,7 +586,7 @@ extension HTTP2StreamStateMachine {
587586
// Sending a DATA frame outside any of these states is a stream error of type STREAM_CLOSED (RFC7540 § 6.1)
588587
case .idle, .halfOpenRemoteLocalIdle, .reservedLocal, .reservedRemote, .halfClosedLocalPeerIdle,
589588
.halfClosedLocalPeerActive, .halfClosedRemoteLocalIdle, .closed:
590-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .streamClosed), effect: nil)
589+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .streamClosed), effect: nil)
591590
}
592591
} catch let error where error is NIOHTTP2Errors.FlowControlViolation {
593592
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .flowControlError), effect: nil)
@@ -658,7 +657,7 @@ extension HTTP2StreamStateMachine {
658657
// Receiving a DATA frame outside any of these states is a stream error of type STREAM_CLOSED (RFC7540 § 6.1)
659658
case .idle, .halfOpenLocalPeerIdle, .reservedLocal, .reservedRemote, .halfClosedLocalPeerIdle,
660659
.halfClosedRemoteLocalActive, .halfClosedRemoteLocalIdle, .closed:
661-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .streamClosed), effect: nil)
660+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .streamClosed), effect: nil)
662661
}
663662
} catch let error where error is NIOHTTP2Errors.FlowControlViolation {
664663
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .flowControlError), effect: nil)
@@ -694,7 +693,7 @@ extension HTTP2StreamStateMachine {
694693
.fullyOpen(localRole: .client, localContentLength: _, remoteContentLength: _, localWindow: _, remoteWindow: _),
695694
.halfClosedRemoteLocalActive(localRole: .client, initiatedBy: _, localContentLength: _, localWindow: _),
696695
.halfClosedRemoteLocalActive(localRole: .server, initiatedBy: .server, localContentLength: _, localWindow: _):
697-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
696+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
698697
}
699698
}
700699

@@ -721,7 +720,7 @@ extension HTTP2StreamStateMachine {
721720
.fullyOpen(localRole: .server, localContentLength: _, remoteContentLength: _, localWindow: _, remoteWindow: _),
722721
.halfClosedLocalPeerActive(localRole: .server, initiatedBy: _, remoteContentLength: _, remoteWindow: _),
723722
.halfClosedLocalPeerActive(localRole: .client, initiatedBy: .server, remoteContentLength: _, remoteWindow: _):
724-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
723+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
725724
}
726725
}
727726

@@ -770,7 +769,7 @@ extension HTTP2StreamStateMachine {
770769
windowEffect = .windowSizeChange(.init(streamID: self.streamID, localStreamWindowSize: nil, remoteStreamWindowSize: Int(remoteWindow)))
771770

772771
case .idle, .reservedLocal, .halfClosedRemoteLocalIdle, .halfClosedRemoteLocalActive, .closed:
773-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
772+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
774773
}
775774
} catch let error where error is NIOHTTP2Errors.InvalidFlowControlWindowSize {
776775
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .flowControlError), effect: nil)
@@ -834,7 +833,7 @@ extension HTTP2StreamStateMachine {
834833
windowEffect = nil
835834

836835
case .idle, .reservedRemote, .closed:
837-
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
836+
return .init(result: .streamError(streamID: self.streamID, underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
838837
}
839838
} catch let error where error is NIOHTTP2Errors.InvalidFlowControlWindowSize {
840839
return .init(result: .streamError(streamID: self.streamID, underlyingError: error, type: .flowControlError), effect: nil)
@@ -857,7 +856,7 @@ extension HTTP2StreamStateMachine {
857856
mutating func receiveRstStream(reason: HTTP2ErrorCode) -> StateMachineResultWithStreamEffect {
858857
// We can receive RST_STREAM frames in any state but idle.
859858
if case .idle = self.state {
860-
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(), type: .protocolError), effect: nil)
859+
return .init(result: .connectionError(underlyingError: NIOHTTP2Errors.BadStreamStateTransition(from: NIOHTTP2StreamState.get(self.state)), type: .protocolError), effect: nil)
861860
}
862861

863862
self.state = .closed(reason: reason)
@@ -1023,3 +1022,58 @@ private extension HPACKHeaders {
10231022
return self.first { $0.name == ":status" }?.value.first == "1"
10241023
}
10251024
}
1025+
1026+
/// A state of a `HTTP2StreamStateMachine`. This copy in effect mirrors `HTTP2StreamStateMachine.state` but without associated values.
1027+
public struct NIOHTTP2StreamState: Hashable, CustomStringConvertible {
1028+
private enum State {
1029+
case idle
1030+
case reservedRemote
1031+
case reservedLocal
1032+
case halfOpenLocalPeerIdle
1033+
case halfOpenRemoteLocalIdle
1034+
case fullyOpen
1035+
case halfClosedLocalPeerIdle
1036+
case halfClosedLocalPeerActive
1037+
case halfClosedRemoteLocalIdle
1038+
case halfClosedRemoteLocalActive
1039+
case closed
1040+
}
1041+
1042+
private var state: State
1043+
1044+
private init(state: State) {
1045+
self.state = state
1046+
}
1047+
1048+
public var description: String {
1049+
return "\(state)"
1050+
}
1051+
1052+
public static let idle = NIOHTTP2StreamState(state: .idle)
1053+
public static let reservedRemote = NIOHTTP2StreamState(state: .reservedRemote)
1054+
public static let reservedLocal = NIOHTTP2StreamState(state: .reservedLocal)
1055+
public static let halfOpenLocalPeerIdle = NIOHTTP2StreamState(state: .halfOpenLocalPeerIdle)
1056+
public static let halfOpenRemoteLocalIdle = NIOHTTP2StreamState(state: .halfOpenRemoteLocalIdle)
1057+
public static let fullyOpen = NIOHTTP2StreamState(state: .fullyOpen)
1058+
public static let halfClosedLocalPeerIdle = NIOHTTP2StreamState(state: .halfClosedLocalPeerIdle)
1059+
public static let halfClosedLocalPeerActive = NIOHTTP2StreamState(state: .halfClosedLocalPeerActive)
1060+
public static let halfClosedRemoteLocalIdle = NIOHTTP2StreamState(state: .halfClosedRemoteLocalIdle)
1061+
public static let halfClosedRemoteLocalActive = NIOHTTP2StreamState(state: .halfClosedRemoteLocalActive)
1062+
public static let closed = NIOHTTP2StreamState(state: .closed)
1063+
1064+
fileprivate static func get(_ state: HTTP2StreamStateMachine.State) -> NIOHTTP2StreamState {
1065+
switch state {
1066+
case .idle: return idle
1067+
case .reservedRemote: return reservedRemote
1068+
case .reservedLocal: return reservedLocal
1069+
case .halfOpenLocalPeerIdle: return halfOpenLocalPeerIdle
1070+
case .halfOpenRemoteLocalIdle: return halfOpenRemoteLocalIdle
1071+
case .fullyOpen: return fullyOpen
1072+
case .halfClosedLocalPeerIdle: return halfClosedLocalPeerIdle
1073+
case .halfClosedLocalPeerActive: return halfClosedLocalPeerActive
1074+
case .halfClosedRemoteLocalIdle: return halfClosedRemoteLocalIdle
1075+
case .halfClosedRemoteLocalActive: return halfClosedRemoteLocalActive
1076+
case .closed: return closed
1077+
}
1078+
}
1079+
}

Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extension ConnectionStateMachineTests {
2727
static var allTests : [(String, (ConnectionStateMachineTests) -> () throws -> Void)] {
2828
return [
2929
("testSimpleRequestResponseFlow", testSimpleRequestResponseFlow),
30+
("testSimpleRequestResponseErrorFlow", testSimpleRequestResponseErrorFlow),
3031
("testOpeningConnectionWhileServerPreambleMissing", testOpeningConnectionWhileServerPreambleMissing),
3132
("testServerSendsItsPreambleFirst", testServerSendsItsPreambleFirst),
3233
("testMoreComplexStreamLifecycle", testMoreComplexStreamLifecycle),

0 commit comments

Comments
 (0)