Skip to content

Commit 6789793

Browse files
committed
Do not open QPACK Encoder Stream if it will not be used
1 parent a646c53 commit 6789793

4 files changed

Lines changed: 127 additions & 48 deletions

File tree

Sources/HTTP3/HTTP3ConnectionStateMachine.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ package struct HTTP3ConnectionStateMachine: ~Copyable {
107107
package init(settings: HTTP3Settings, type: HTTP3ConnectionType) {
108108
let qpackState = QPACKStateMachine(
109109
decoderMaxTableSize: Int(clamping: settings.qpackMaximumTableCapacity),
110-
decoderMaxBlockedStreams: Int(clamping: settings.qpackBlockedStreams)
110+
decoderMaxBlockedStreams: Int(clamping: settings.qpackBlockedStreams),
111+
localEncoderMaxTableCapacity: Int(clamping: settings.qpackMaximumTableCapacity)
111112
)
112113
self.init(state: .notStarted(.init(qpackState: qpackState, localSettings: settings, type: type)))
113114
}

Sources/HTTP3/QPACK/QPACKStateMachine.swift

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,32 @@ package struct QPACKStateMachine: ~Copyable {
5757
}
5858

5959
private let state: State
60+
private let localMaxTableCapacity: Int
6061

61-
private init(state: consuming State) {
62+
private init(state: consuming State, localMaxTableCapacity: Int) {
6263
self.state = state
64+
self.localMaxTableCapacity = localMaxTableCapacity
6365
}
6466

65-
init() {
66-
self.init(state: .initial(.init(encoder: .init())))
67+
init(localMaxTableCapacity: Int) {
68+
self.init(state: .initial(.init(encoder: .init())), localMaxTableCapacity: localMaxTableCapacity)
6769
}
6870

6971
mutating func receivedRemoteSettings(
7072
maxQueueSize: Int,
7173
dynamicTableSize: Int
7274
) -> GotRemoteSettingsAction? {
75+
let localMaxTableCapacity = self.localMaxTableCapacity
7376
switch consume self.state {
7477
case .initial(let initial):
7578
// RFC 9204 § 4.2: An endpoint MAY avoid creating an encoder stream if it will not be used
76-
if dynamicTableSize == 0 {
79+
// This should include if the local table will not be used either
80+
if dynamicTableSize == 0 || localMaxTableCapacity == 0 {
7781
self = .init(
7882
state: .withoutDynamic(
7983
.init(encoder: initial.encoder)
80-
)
84+
),
85+
localMaxTableCapacity: localMaxTableCapacity
8186
)
8287
return nil
8388
} else {
@@ -88,7 +93,8 @@ package struct QPACKStateMachine: ~Copyable {
8893
maxQueueSize: maxQueueSize,
8994
dynamicTableSize: dynamicTableSize
9095
)
91-
)
96+
),
97+
localMaxTableCapacity: localMaxTableCapacity
9298
)
9399
return .makeEncoderInstructionStream
94100
}
@@ -109,6 +115,7 @@ package struct QPACKStateMachine: ~Copyable {
109115
}
110116

111117
mutating func outboundEncoderStreamReady() -> OutboundEncoderStreamReadyAction {
118+
let localMaxTableCapacity = self.localMaxTableCapacity
112119
switch consume self.state {
113120
case .initial:
114121
fatalError("Encoder stream created when not needed or already made")
@@ -128,35 +135,37 @@ package struct QPACKStateMachine: ~Copyable {
128135
.init(
129136
encoder: dynamicEncoder
130137
)
131-
)
138+
),
139+
localMaxTableCapacity: localMaxTableCapacity
132140
)
133141
return .sendEncoderInstruction(instruction)
134142
}
135143
}
136144

137145
mutating func encodeHeaders(_ headers: [HTTPField], forStream streamID: QUICStreamID) -> QPACKEncodeResult {
146+
let localMaxTableCapacity = self.localMaxTableCapacity
138147
switch consume self.state {
139148
case .initial(let initial):
140149
let result = QPACKEncodeResult(fieldSection: initial.encoder.encode(headers: headers), instructions: [])
141-
self = .init(state: .initial(initial))
150+
self = .init(state: .initial(initial), localMaxTableCapacity: localMaxTableCapacity)
142151
return result
143152
case .awaitingStream(let awaiting):
144153
let result = QPACKEncodeResult(
145154
fieldSection: awaiting.encoder.encode(headers: headers),
146155
instructions: []
147156
)
148-
self = .init(state: .awaitingStream(awaiting))
157+
self = .init(state: .awaitingStream(awaiting), localMaxTableCapacity: localMaxTableCapacity)
149158
return result
150159
case .withoutDynamic(let woDynamic):
151160
let result = QPACKEncodeResult(
152161
fieldSection: woDynamic.encoder.encode(headers: headers),
153162
instructions: []
154163
)
155-
self = .init(state: .withoutDynamic(woDynamic))
164+
self = .init(state: .withoutDynamic(woDynamic), localMaxTableCapacity: localMaxTableCapacity)
156165
return result
157166
case .withDynamic(var withDynamic):
158167
let result = withDynamic.encoder.encode(headers: headers, forStream: streamID)
159-
self = .init(state: .withDynamic(withDynamic))
168+
self = .init(state: .withDynamic(withDynamic), localMaxTableCapacity: localMaxTableCapacity)
160169
return result
161170
}
162171
}
@@ -178,20 +187,21 @@ package struct QPACKStateMachine: ~Copyable {
178187
location: location
179188
)
180189
}
190+
let localMaxTableCapacity = self.localMaxTableCapacity
181191
switch consume self.state {
182192
case .initial(let initial):
183-
self = .init(state: .initial(initial))
193+
self = .init(state: .initial(initial), localMaxTableCapacity: localMaxTableCapacity)
184194
return .emitConnectionError(noDynamicTableError(location: .here()))
185195
case .awaitingStream(let awaitingStream):
186-
self = .init(state: .awaitingStream(awaitingStream))
196+
self = .init(state: .awaitingStream(awaitingStream), localMaxTableCapacity: localMaxTableCapacity)
187197
return .emitConnectionError(noDynamicTableError(location: .here()))
188198
case .withoutDynamic(let withoutDynamic):
189-
self = .init(state: .withoutDynamic(withoutDynamic))
199+
self = .init(state: .withoutDynamic(withoutDynamic), localMaxTableCapacity: localMaxTableCapacity)
190200
return .emitConnectionError(noDynamicTableError(location: .here()))
191201
case .withDynamic(var withDynamic):
192202
do {
193203
try withDynamic.encoder.processInstruction(instruction)
194-
self = .init(state: .withDynamic(withDynamic))
204+
self = .init(state: .withDynamic(withDynamic), localMaxTableCapacity: localMaxTableCapacity)
195205
return nil
196206
} catch {
197207
@inline(never)
@@ -208,7 +218,7 @@ package struct QPACKStateMachine: ~Copyable {
208218
)
209219
}
210220
// TODO: move to error state?
211-
self = .init(state: .withDynamic(withDynamic))
221+
self = .init(state: .withDynamic(withDynamic), localMaxTableCapacity: localMaxTableCapacity)
212222
return .emitConnectionError(invalidDecoderInstructionError(cause: error, location: .here()))
213223
}
214224
}
@@ -284,8 +294,8 @@ package struct QPACKStateMachine: ~Copyable {
284294
private var decoderQueue: FieldSectionQueue
285295
private var outboundDecoderInstructionQueue: OutboundDecoderInstructionQueue
286296

287-
package init(decoderMaxTableSize: Int, decoderMaxBlockedStreams: Int) {
288-
self.encoderState = .init()
297+
package init(decoderMaxTableSize: Int, decoderMaxBlockedStreams: Int, localEncoderMaxTableCapacity: Int = 0) {
298+
self.encoderState = .init(localMaxTableCapacity: localEncoderMaxTableCapacity)
289299
self.qpackDecoder = .init(dynamicTableMaxCapacity: decoderMaxTableSize)
290300
self.decoderQueue = .init(maxItems: decoderMaxBlockedStreams)
291301
self.outboundDecoderInstructionQueue = .init()

Tests/HTTP3Tests/HTTP3ConnectionStateMachineTests.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,11 @@ struct HTTP3ConnectionStateMachineTests {
369369
let action1 = stateMachine.initialize()
370370
#expect(action1 == .createControlStream)
371371

372+
// Local settings have qpackMaximumTableCapacity = 0, so we will not use the dynamic table
373+
// as an encoder even if the remote offers capacity. Per RFC 9204 § 4.2, we may skip the
374+
// encoder stream if it will not be used.
372375
let action2 = stateMachine.receivedControlFrame(.settings(remoteSettings))
373-
guard case .makeEncoderInstructionStream = action2 else {
374-
Issue.record("Unexpected action \(String(describing: action2))")
375-
return
376-
}
377-
378-
let action3 = stateMachine.outboundEncoderStreamReady(streamID: 3)
379-
#expect(action3 == .sendEncoderInstruction(.setDynamicTableCapacity(100)))
376+
#expect(action2 == nil)
380377
}
381378

382379
@Test
@@ -733,21 +730,24 @@ struct HTTP3ConnectionStateMachineTests {
733730

734731
@Test
735732
func testEncoderStreamReadyAfterShutdown() {
736-
let remoteSettings = HTTP3Settings(qpackMaximumTableCapacity: 100)
737-
var stateMachine = HTTP3ConnectionStateMachine(settings: .init(), type: .client)
733+
let settings = HTTP3Settings(qpackMaximumTableCapacity: 100)
734+
var stateMachine = HTTP3ConnectionStateMachine(settings: settings, type: .client)
735+
var idGenerator = IDGenerator(type: .client)
738736

739737
let action1 = stateMachine.initialize()
740-
#expect(action1 == .createControlStream)
738+
#expect(action1 == .createControlAndDecoderStreams)
739+
stateMachine.outboundControlStreamReady(streamID: idGenerator.outboundUni())
740+
_ = stateMachine.outboundDecoderStreamReady(streamID: idGenerator.outboundUni())
741741

742-
let action2 = stateMachine.receivedControlFrame(.settings(remoteSettings))
742+
let action2 = stateMachine.receivedControlFrame(.settings(settings))
743743
guard case .makeEncoderInstructionStream = action2 else {
744744
Issue.record("Unexpected action \(String(describing: action2))")
745745
return
746746
}
747747

748748
#expect(stateMachine.shutdownConnectionImmediately() == .shutdown)
749749

750-
let action3 = stateMachine.outboundEncoderStreamReady(streamID: 2)
750+
let action3 = stateMachine.outboundEncoderStreamReady(streamID: idGenerator.outboundUni())
751751
#expect(action3 == nil) // We don't send our settings because we shutdown
752752
}
753753

0 commit comments

Comments
 (0)