Skip to content

Add a frame delegate #522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions Sources/NIOHTTP2/HTTP2ChannelHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
/// The maximum number of sequential CONTINUATION frames.
private let maximumSequentialContinuationFrames: Int

/// A delegate which is told about frames which have been written.
private let frameDelegate: NIOHTTP2FrameDelegate?

@usableFromInline
internal var inboundStreamMultiplexer: InboundStreamMultiplexer? {
self.inboundStreamMultiplexerState.multiplexer
Expand Down Expand Up @@ -242,7 +245,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
maximumResetFrameCount: 200,
resetFrameCounterWindow: .seconds(30),
maximumStreamErrorCount: 200,
streamErrorCounterWindow: .seconds(30)
streamErrorCounterWindow: .seconds(30),
frameDelegate: nil
)
}

Expand Down Expand Up @@ -280,7 +284,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
maximumResetFrameCount: 200,
resetFrameCounterWindow: .seconds(30),
maximumStreamErrorCount: 200,
streamErrorCounterWindow: .seconds(30)
streamErrorCounterWindow: .seconds(30),
frameDelegate: nil
)

}
Expand All @@ -295,6 +300,27 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
mode: ParserMode,
connectionConfiguration: ConnectionConfiguration = .init(),
streamConfiguration: StreamConfiguration = .init()
) {
self.init(
mode: mode,
frameDelegate: nil,
connectionConfiguration: connectionConfiguration,
streamConfiguration: streamConfiguration
)
}

/// Constructs a ``NIOHTTP2Handler``.
///
/// - Parameters:
/// - mode: The mode for this handler, client or server.
/// - frameDelegate: A delegate which is notified about frames being written.
/// - connectionConfiguration: The settings that will be used when establishing the connection.
/// - streamConfiguration: The settings that will be used when establishing new streams.
public convenience init(
mode: ParserMode,
frameDelegate: NIOHTTP2FrameDelegate?,
connectionConfiguration: ConnectionConfiguration = ConnectionConfiguration(),
streamConfiguration: StreamConfiguration = StreamConfiguration()
) {
self.init(
mode: mode,
Expand All @@ -310,7 +336,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
frameDelegate: frameDelegate
)
}

Expand All @@ -328,7 +355,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
maximumResetFrameCount: Int,
resetFrameCounterWindow: TimeAmount,
maximumStreamErrorCount: Int,
streamErrorCounterWindow: TimeAmount
streamErrorCounterWindow: TimeAmount,
frameDelegate: NIOHTTP2FrameDelegate?
) {
self._eventLoop = eventLoop
self.stateMachine = HTTP2ConnectionStateMachine(
Expand All @@ -355,6 +383,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
self.inboundStreamMultiplexerState = .uninitializedLegacy
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
self.frameDelegate = frameDelegate
}

/// Constructs a ``NIOHTTP2Handler``.
Expand Down Expand Up @@ -391,7 +420,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
resetFrameCounterWindow: TimeAmount = .seconds(30),
maximumStreamErrorCount: Int = 200,
streamErrorCounterWindow: TimeAmount = .seconds(30),
maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches
maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches,
frameDelegate: NIOHTTP2FrameDelegate? = nil
) {
self.stateMachine = HTTP2ConnectionStateMachine(
role: .init(mode),
Expand All @@ -418,6 +448,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
self.inboundStreamMultiplexerState = .uninitializedLegacy
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
self.frameDelegate = frameDelegate
}

public func handlerAdded(context: ChannelHandlerContext) {
Expand Down Expand Up @@ -1067,6 +1098,11 @@ extension NIOHTTP2Handler {
return
}

// Tell the delegate, if there is one.
if let delegate = self.frameDelegate {
delegate.wroteFrame(frame)
}

// Ok, if we got here we're good to send data. We want to attach the promise to the latest write, not
// always the frame header.
self.wroteFrame = true
Expand Down Expand Up @@ -1391,7 +1427,8 @@ extension NIOHTTP2Handler {
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
frameDelegate: nil
)

self.inboundStreamMultiplexerState = .uninitializedInline(
Expand All @@ -1408,6 +1445,7 @@ extension NIOHTTP2Handler {
connectionConfiguration: ConnectionConfiguration = .init(),
streamConfiguration: StreamConfiguration = .init(),
streamDelegate: NIOHTTP2StreamDelegate? = nil,
frameDelegate: NIOHTTP2FrameDelegate?,
inboundStreamInitializerWithAnyOutput: @escaping StreamInitializerWithAnyOutput
) {
self.init(
Expand All @@ -1424,7 +1462,8 @@ extension NIOHTTP2Handler {
maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength
streamErrorCounterWindow: streamConfiguration.streamErrorRateLimit.windowLength,
frameDelegate: frameDelegate
)
self.inboundStreamMultiplexerState = .uninitializedAsync(
streamConfiguration,
Expand Down
37 changes: 37 additions & 0 deletions Sources/NIOHTTP2/HTTP2PipelineHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -866,13 +866,50 @@ extension ChannelPipeline.SynchronousOperations {
streamDelegate: NIOHTTP2StreamDelegate?,
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
try self.configureAsyncHTTP2Pipeline(
mode: mode,
streamDelegate: streamDelegate,
frameDelegate: nil,
configuration: configuration,
streamInitializer: streamInitializer
)
}

/// Configures a `ChannelPipeline` to speak HTTP/2 and sets up mapping functions so that it may be interacted with from concurrent code.
///
/// This operation **must** be called on the event loop.
///
/// In general this is not entirely useful by itself, as HTTP/2 is a negotiated protocol. This helper does not handle negotiation.
/// Instead, this simply adds the handler required to speak HTTP/2 after negotiation has completed, or when agreed by prior knowledge.
/// Use this function to setup a HTTP/2 pipeline if you wish to use async sequence abstractions over inbound and outbound streams,
/// as it allows that pipeline to evolve without breaking your code.
///
/// - Parameters:
/// - mode: The mode this pipeline will operate in, server or client.
/// - streamDelegate: A delegate which is called when streams are created and closed.
/// - frameDelegate: A delegate which is called when frames are written to the network.
/// - configuration: The settings that will be used when establishing the connection and new streams.
/// - streamInitializer: A closure that will be called whenever the remote peer initiates a new stream.
/// The output of this closure is the element type of the returned multiplexer
/// - Returns: An `EventLoopFuture` containing the `AsyncStreamMultiplexer` inserted into this pipeline, which can
/// be used to initiate new streams and iterate over inbound HTTP/2 stream channels.
@inlinable
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func configureAsyncHTTP2Pipeline<Output: Sendable>(
mode: NIOHTTP2Handler.ParserMode,
streamDelegate: NIOHTTP2StreamDelegate?,
frameDelegate: NIOHTTP2FrameDelegate?,
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
let handler = NIOHTTP2Handler(
mode: mode,
eventLoop: self.eventLoop,
connectionConfiguration: configuration.connection,
streamConfiguration: configuration.stream,
streamDelegate: streamDelegate,
frameDelegate: frameDelegate,
inboundStreamInitializerWithAnyOutput: { channel in
streamInitializer(channel).map { $0 }
}
Expand Down
30 changes: 30 additions & 0 deletions Sources/NIOHTTP2/NIOHTTP2FrameDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOHPACK

/// A delegate which can be used with the ``NIOHTTP2Handler`` which is notified
/// when various frame types are written into the connection channel.
///
/// This delegate, when used by the ``NIOHTTP2Handler`` will be called on the event
/// loop associated with the channel that the handler is a part of. As such you should
/// avoid doing expensive or blocking work in this delegate.
public protocol NIOHTTP2FrameDelegate {
/// Called when a frame is written by the connection channel.
///
/// - Parameters:
/// - frame: The frame to write.
func wroteFrame(_ frame: HTTP2Frame)
}
Loading