Skip to content

Commit 5e9e99e

Browse files
authored
Add a frame delegate (#522)
Motivation: Frames written from a child channel may not be written immediately by the connection channel. For example, a DATA frame written on a stream may be larger than the max frame size imposed on the connection and so the connection channel will have to slice it into multiple frames. Or the connection may not be writable and may delay the transmission of the frame. This behaviour isn't currently observable but is useful to know about. Modifications: - Add a frame delegate which is notified when certain frames are written by the connection channel. Result: Users can observer when headers and data frames are written into the connection channel.
1 parent f0e9452 commit 5e9e99e

File tree

4 files changed

+262
-11
lines changed

4 files changed

+262
-11
lines changed

Sources/NIOHTTP2/HTTP2ChannelHandler.swift

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
113113
/// The maximum number of sequential CONTINUATION frames.
114114
private let maximumSequentialContinuationFrames: Int
115115

116+
/// A delegate which is told about frames which have been written.
117+
private let frameDelegate: NIOHTTP2FrameDelegate?
118+
116119
@usableFromInline
117120
internal var inboundStreamMultiplexer: InboundStreamMultiplexer? {
118121
self.inboundStreamMultiplexerState.multiplexer
@@ -242,7 +245,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
242245
maximumResetFrameCount: 200,
243246
resetFrameCounterWindow: .seconds(30),
244247
maximumStreamErrorCount: 200,
245-
streamErrorCounterWindow: .seconds(30)
248+
streamErrorCounterWindow: .seconds(30),
249+
frameDelegate: nil
246250
)
247251
}
248252

@@ -280,7 +284,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
280284
maximumResetFrameCount: 200,
281285
resetFrameCounterWindow: .seconds(30),
282286
maximumStreamErrorCount: 200,
283-
streamErrorCounterWindow: .seconds(30)
287+
streamErrorCounterWindow: .seconds(30),
288+
frameDelegate: nil
284289
)
285290

286291
}
@@ -295,6 +300,27 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
295300
mode: ParserMode,
296301
connectionConfiguration: ConnectionConfiguration = .init(),
297302
streamConfiguration: StreamConfiguration = .init()
303+
) {
304+
self.init(
305+
mode: mode,
306+
frameDelegate: nil,
307+
connectionConfiguration: connectionConfiguration,
308+
streamConfiguration: streamConfiguration
309+
)
310+
}
311+
312+
/// Constructs a ``NIOHTTP2Handler``.
313+
///
314+
/// - Parameters:
315+
/// - mode: The mode for this handler, client or server.
316+
/// - frameDelegate: A delegate which is notified about frames being written.
317+
/// - connectionConfiguration: The settings that will be used when establishing the connection.
318+
/// - streamConfiguration: The settings that will be used when establishing new streams.
319+
public convenience init(
320+
mode: ParserMode,
321+
frameDelegate: NIOHTTP2FrameDelegate?,
322+
connectionConfiguration: ConnectionConfiguration = ConnectionConfiguration(),
323+
streamConfiguration: StreamConfiguration = StreamConfiguration()
298324
) {
299325
self.init(
300326
mode: mode,
@@ -310,7 +336,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
310336
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
311337
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
312338
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
313-
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
339+
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
340+
frameDelegate: frameDelegate
314341
)
315342
}
316343

@@ -328,7 +355,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
328355
maximumResetFrameCount: Int,
329356
resetFrameCounterWindow: TimeAmount,
330357
maximumStreamErrorCount: Int,
331-
streamErrorCounterWindow: TimeAmount
358+
streamErrorCounterWindow: TimeAmount,
359+
frameDelegate: NIOHTTP2FrameDelegate?
332360
) {
333361
self._eventLoop = eventLoop
334362
self.stateMachine = HTTP2ConnectionStateMachine(
@@ -355,6 +383,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
355383
self.inboundStreamMultiplexerState = .uninitializedLegacy
356384
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
357385
self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
386+
self.frameDelegate = frameDelegate
358387
}
359388

360389
/// Constructs a ``NIOHTTP2Handler``.
@@ -391,7 +420,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
391420
resetFrameCounterWindow: TimeAmount = .seconds(30),
392421
maximumStreamErrorCount: Int = 200,
393422
streamErrorCounterWindow: TimeAmount = .seconds(30),
394-
maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches
423+
maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches,
424+
frameDelegate: NIOHTTP2FrameDelegate? = nil
395425
) {
396426
self.stateMachine = HTTP2ConnectionStateMachine(
397427
role: .init(mode),
@@ -418,6 +448,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
418448
self.inboundStreamMultiplexerState = .uninitializedLegacy
419449
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
420450
self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
451+
self.frameDelegate = frameDelegate
421452
}
422453

423454
public func handlerAdded(context: ChannelHandlerContext) {
@@ -1067,6 +1098,11 @@ extension NIOHTTP2Handler {
10671098
return
10681099
}
10691100

1101+
// Tell the delegate, if there is one.
1102+
if let delegate = self.frameDelegate {
1103+
delegate.wroteFrame(frame)
1104+
}
1105+
10701106
// Ok, if we got here we're good to send data. We want to attach the promise to the latest write, not
10711107
// always the frame header.
10721108
self.wroteFrame = true
@@ -1391,7 +1427,8 @@ extension NIOHTTP2Handler {
13911427
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
13921428
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
13931429
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
1394-
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
1430+
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
1431+
frameDelegate: nil
13951432
)
13961433

13971434
self.inboundStreamMultiplexerState = .uninitializedInline(
@@ -1408,6 +1445,7 @@ extension NIOHTTP2Handler {
14081445
connectionConfiguration: ConnectionConfiguration = .init(),
14091446
streamConfiguration: StreamConfiguration = .init(),
14101447
streamDelegate: NIOHTTP2StreamDelegate? = nil,
1448+
frameDelegate: NIOHTTP2FrameDelegate?,
14111449
inboundStreamInitializerWithAnyOutput: @escaping StreamInitializerWithAnyOutput
14121450
) {
14131451
self.init(
@@ -1424,7 +1462,8 @@ extension NIOHTTP2Handler {
14241462
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
14251463
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
14261464
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
1427-
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
1465+
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
1466+
frameDelegate: frameDelegate
14281467
)
14291468
self.inboundStreamMultiplexerState = .uninitializedAsync(
14301469
streamConfiguration,

Sources/NIOHTTP2/HTTP2PipelineHelpers.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,13 +866,50 @@ extension ChannelPipeline.SynchronousOperations {
866866
streamDelegate: NIOHTTP2StreamDelegate?,
867867
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
868868
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
869+
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
870+
try self.configureAsyncHTTP2Pipeline(
871+
mode: mode,
872+
streamDelegate: streamDelegate,
873+
frameDelegate: nil,
874+
configuration: configuration,
875+
streamInitializer: streamInitializer
876+
)
877+
}
878+
879+
/// Configures a `ChannelPipeline` to speak HTTP/2 and sets up mapping functions so that it may be interacted with from concurrent code.
880+
///
881+
/// This operation **must** be called on the event loop.
882+
///
883+
/// In general this is not entirely useful by itself, as HTTP/2 is a negotiated protocol. This helper does not handle negotiation.
884+
/// Instead, this simply adds the handler required to speak HTTP/2 after negotiation has completed, or when agreed by prior knowledge.
885+
/// Use this function to setup a HTTP/2 pipeline if you wish to use async sequence abstractions over inbound and outbound streams,
886+
/// as it allows that pipeline to evolve without breaking your code.
887+
///
888+
/// - Parameters:
889+
/// - mode: The mode this pipeline will operate in, server or client.
890+
/// - streamDelegate: A delegate which is called when streams are created and closed.
891+
/// - frameDelegate: A delegate which is called when frames are written to the network.
892+
/// - configuration: The settings that will be used when establishing the connection and new streams.
893+
/// - streamInitializer: A closure that will be called whenever the remote peer initiates a new stream.
894+
/// The output of this closure is the element type of the returned multiplexer
895+
/// - Returns: An `EventLoopFuture` containing the `AsyncStreamMultiplexer` inserted into this pipeline, which can
896+
/// be used to initiate new streams and iterate over inbound HTTP/2 stream channels.
897+
@inlinable
898+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
899+
public func configureAsyncHTTP2Pipeline<Output: Sendable>(
900+
mode: NIOHTTP2Handler.ParserMode,
901+
streamDelegate: NIOHTTP2StreamDelegate?,
902+
frameDelegate: NIOHTTP2FrameDelegate?,
903+
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
904+
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
869905
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
870906
let handler = NIOHTTP2Handler(
871907
mode: mode,
872908
eventLoop: self.eventLoop,
873909
connectionConfiguration: configuration.connection,
874910
streamConfiguration: configuration.stream,
875911
streamDelegate: streamDelegate,
912+
frameDelegate: frameDelegate,
876913
inboundStreamInitializerWithAnyOutput: { channel in
877914
streamInitializer(channel).map { $0 }
878915
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOHPACK
17+
18+
/// A delegate which can be used with the ``NIOHTTP2Handler`` which is notified
19+
/// when various frame types are written into the connection channel.
20+
///
21+
/// This delegate, when used by the ``NIOHTTP2Handler`` will be called on the event
22+
/// loop associated with the channel that the handler is a part of. As such you should
23+
/// avoid doing expensive or blocking work in this delegate.
24+
public protocol NIOHTTP2FrameDelegate {
25+
/// Called when a frame is written by the connection channel.
26+
///
27+
/// - Parameters:
28+
/// - frame: The frame to write.
29+
func wroteFrame(_ frame: HTTP2Frame)
30+
}

0 commit comments

Comments
 (0)