Skip to content

Commit 8d8eb60

Browse files
authored
Add a variant of configureAsyncHTTP2Pipeline which takes a stream delegate (#439)
Motivation: `configureAsyncHTTP2Pipeline` doesn't allow a stream delegate to be specified. As the async pipeline uses the "inline" stream multiplexer there's no way to account for streams within the connection channel. Modifications: - Add a sync and async variants of `configureAsyncHTTP2Pipeline` which accepts an optional stream delegate - Rewrite the existing helpers in terms of the new one Result: Users can configure an async http pipeline with a stream delegate.
1 parent 356a3af commit 8d8eb60

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

Sources/NIOHTTP2/HTTP2PipelineHelpers.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,11 +490,43 @@ extension Channel {
490490
mode: NIOHTTP2Handler.ParserMode,
491491
configuration: NIOHTTP2Handler.Configuration = .init(),
492492
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
493+
) -> EventLoopFuture<NIOHTTP2Handler.AsyncStreamMultiplexer<Output>> {
494+
self.configureAsyncHTTP2Pipeline(
495+
mode: mode,
496+
streamDelegate: nil,
497+
configuration: configuration,
498+
streamInitializer: streamInitializer
499+
)
500+
}
501+
502+
/// Configures a `ChannelPipeline` to speak HTTP/2 and sets up mapping functions so that it may be interacted with from concurrent code.
503+
///
504+
/// In general this is not entirely useful by itself, as HTTP/2 is a negotiated protocol. This helper does not handle negotiation.
505+
/// Instead, this simply adds the handler required to speak HTTP/2 after negotiation has completed, or when agreed by prior knowledge.
506+
/// Use this function to setup a HTTP/2 pipeline if you wish to use async sequence abstractions over inbound and outbound streams.
507+
/// Using this rather than implementing a similar function yourself allows that pipeline to evolve without breaking your code.
508+
///
509+
/// - Parameters:
510+
/// - mode: The mode this pipeline will operate in, server or client.
511+
/// - streamDelegate: A delegate which is called when streams are created and closed.
512+
/// - configuration: The settings that will be used when establishing the connection and new streams.
513+
/// - streamInitializer: A closure that will be called whenever the remote peer initiates a new stream.
514+
/// The output of this closure is the element type of the returned multiplexer
515+
/// - Returns: An `EventLoopFuture` containing the `AsyncStreamMultiplexer` inserted into this pipeline, which can
516+
/// be used to initiate new streams and iterate over inbound HTTP/2 stream channels.
517+
@inlinable
518+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
519+
public func configureAsyncHTTP2Pipeline<Output: Sendable>(
520+
mode: NIOHTTP2Handler.ParserMode,
521+
streamDelegate: NIOHTTP2StreamDelegate?,
522+
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
523+
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
493524
) -> EventLoopFuture<NIOHTTP2Handler.AsyncStreamMultiplexer<Output>> {
494525
if self.eventLoop.inEventLoop {
495526
return self.eventLoop.makeCompletedFuture {
496527
return try self.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
497528
mode: mode,
529+
streamDelegate: streamDelegate,
498530
configuration: configuration,
499531
streamInitializer: streamInitializer
500532
)
@@ -503,13 +535,15 @@ extension Channel {
503535
return self.eventLoop.submit {
504536
return try self.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
505537
mode: mode,
538+
streamDelegate: streamDelegate,
506539
configuration: configuration,
507540
streamInitializer: streamInitializer
508541
)
509542
}
510543
}
511544
}
512545

546+
513547
/// Configures a channel to perform an HTTP/2 secure upgrade with typed negotiation results.
514548
///
515549
/// HTTP/2 secure upgrade uses the Application Layer Protocol Negotiation TLS extension to
@@ -642,12 +676,46 @@ extension ChannelPipeline.SynchronousOperations {
642676
mode: NIOHTTP2Handler.ParserMode,
643677
configuration: NIOHTTP2Handler.Configuration = .init(),
644678
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
679+
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
680+
try self.configureAsyncHTTP2Pipeline(
681+
mode: mode,
682+
streamDelegate: nil,
683+
configuration: configuration,
684+
streamInitializer: streamInitializer
685+
)
686+
}
687+
688+
/// Configures a `ChannelPipeline` to speak HTTP/2 and sets up mapping functions so that it may be interacted with from concurrent code.
689+
///
690+
/// This operation **must** be called on the event loop.
691+
///
692+
/// In general this is not entirely useful by itself, as HTTP/2 is a negotiated protocol. This helper does not handle negotiation.
693+
/// Instead, this simply adds the handler required to speak HTTP/2 after negotiation has completed, or when agreed by prior knowledge.
694+
/// Use this function to setup a HTTP/2 pipeline if you wish to use async sequence abstractions over inbound and outbound streams,
695+
/// as it allows that pipeline to evolve without breaking your code.
696+
///
697+
/// - Parameters:
698+
/// - mode: The mode this pipeline will operate in, server or client.
699+
/// - streamDelegate: A delegate which is called when streams are created and closed.
700+
/// - configuration: The settings that will be used when establishing the connection and new streams.
701+
/// - streamInitializer: A closure that will be called whenever the remote peer initiates a new stream.
702+
/// The output of this closure is the element type of the returned multiplexer
703+
/// - Returns: An `EventLoopFuture` containing the `AsyncStreamMultiplexer` inserted into this pipeline, which can
704+
/// be used to initiate new streams and iterate over inbound HTTP/2 stream channels.
705+
@inlinable
706+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
707+
public func configureAsyncHTTP2Pipeline<Output: Sendable>(
708+
mode: NIOHTTP2Handler.ParserMode,
709+
streamDelegate: NIOHTTP2StreamDelegate?,
710+
configuration: NIOHTTP2Handler.Configuration = NIOHTTP2Handler.Configuration(),
711+
streamInitializer: @escaping NIOChannelInitializerWithOutput<Output>
645712
) throws -> NIOHTTP2Handler.AsyncStreamMultiplexer<Output> {
646713
let handler = NIOHTTP2Handler(
647714
mode: mode,
648715
eventLoop: self.eventLoop,
649716
connectionConfiguration: configuration.connection,
650717
streamConfiguration: configuration.stream,
718+
streamDelegate: streamDelegate,
651719
inboundStreamInitializerWithAnyOutput: { channel in
652720
streamInitializer(channel).map { return $0 }
653721
}

Tests/NIOHTTP2Tests/ConfiguringPipelineAsyncMultiplexerTests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,47 @@ final class ConfiguringPipelineAsyncMultiplexerTests: XCTestCase {
466466
}
467467
}
468468

469+
func testAsyncPipelineConfiguresStreamDelegate() async throws {
470+
let clientRecorder = StreamRecorder()
471+
let clientMultiplexer = try await self.clientChannel.configureAsyncHTTP2Pipeline(
472+
mode: .client,
473+
streamDelegate: clientRecorder
474+
) { channel in
475+
channel.eventLoop.makeSucceededVoidFuture()
476+
}.get()
477+
478+
let serverRecorder = StreamRecorder()
479+
_ = try await self.serverChannel.configureAsyncHTTP2Pipeline(
480+
mode: .server,
481+
streamDelegate: serverRecorder
482+
) { channel in
483+
channel.pipeline.addHandler(OKResponder())
484+
}.get()
485+
486+
try await self.assertDoHandshake(client: self.clientChannel, server: self.serverChannel)
487+
488+
for _ in 0 ..< 3 {
489+
try await clientMultiplexer.openStream { stream in
490+
return stream.pipeline.addHandlers(SimpleRequest())
491+
}
492+
493+
try await Self.deliverAllBytes(from: self.clientChannel, to: self.serverChannel)
494+
try await Self.deliverAllBytes(from: self.serverChannel, to: self.clientChannel)
495+
}
496+
497+
let expected: [StreamRecorder.Event] = [
498+
.init(streamID: 1, operation: .opened), .init(streamID: 1, operation: .closed),
499+
.init(streamID: 3, operation: .opened), .init(streamID: 3, operation: .closed),
500+
.init(streamID: 5, operation: .opened), .init(streamID: 5, operation: .closed),
501+
]
502+
503+
XCTAssertEqual(clientRecorder.events, expected)
504+
XCTAssertEqual(serverRecorder.events, expected)
505+
506+
try await self.clientChannel.close()
507+
try await self.serverChannel.close()
508+
}
509+
469510
// Simple handler which maps server response parts to remove references to `IOData` which isn't Sendable
470511
internal final class HTTP1ServerSendability: ChannelOutboundHandler {
471512
public typealias ResponsePart = HTTPPart<HTTPResponseHead, ByteBuffer>
@@ -503,6 +544,40 @@ final class ConfiguringPipelineAsyncMultiplexerTests: XCTestCase {
503544
context.fireChannelRead(data)
504545
}
505546
}
547+
548+
final class StreamRecorder: NIOHTTP2StreamDelegate, Sendable {
549+
private let _events: NIOLockedValueBox<[Event]>
550+
551+
struct Event: Sendable, Hashable {
552+
var streamID: HTTP2StreamID
553+
var operation: Operation
554+
}
555+
556+
enum Operation: Sendable, Hashable {
557+
case opened
558+
case closed
559+
}
560+
561+
var events: [Event] {
562+
self._events.withLockedValue { $0 }
563+
}
564+
565+
init() {
566+
self._events = NIOLockedValueBox([])
567+
}
568+
569+
func streamCreated(_ id: HTTP2StreamID, channel: any Channel) {
570+
self._events.withLockedValue {
571+
$0.append(Event(streamID: id, operation: .opened))
572+
}
573+
}
574+
575+
func streamClosed(_ id: HTTP2StreamID, channel: any Channel) {
576+
self._events.withLockedValue {
577+
$0.append(Event(streamID: id, operation: .closed))
578+
}
579+
}
580+
}
506581
}
507582

508583
#if compiler(<5.9)

0 commit comments

Comments
 (0)