Skip to content

Commit

Permalink
Server Channel configuration and HTTP1 timeout (#600)
Browse files Browse the repository at this point in the history
* Add HTTP1Channel.Configuration and add idleTimeout to it

* HTTP2 channel configuration

* Fix comment

* Update Sources/HummingbirdHTTP2/HTTP2Channel.swift

Co-authored-by: Joannis Orlandos <[email protected]>

* Update Sources/HummingbirdHTTP2/HTTP2Channel.swift

Co-authored-by: Joannis Orlandos <[email protected]>

* Update comment about additionalChannelHandlers

---------

Co-authored-by: Joannis Orlandos <[email protected]>
  • Loading branch information
adam-fowler and Joannis authored Nov 6, 2024
1 parent a3283ff commit 61a7d64
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 55 deletions.
52 changes: 44 additions & 8 deletions Sources/HummingbirdCore/Server/HTTP/HTTP1Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,50 @@ import NIOHTTPTypesHTTP1
public struct HTTP1Channel: ServerChildChannel, HTTPChannelHandler {
public typealias Value = NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>

/// HTTP1Channel configuration
public struct Configuration: Sendable {
/// Additional channel handlers to add to channel after HTTP part decoding and before HTTP request processing
public var additionalChannelHandlers: @Sendable () -> [any RemovableChannelHandler]
/// Time before closing an idle channel
public var idleTimeout: TimeAmount?

/// Initialize HTTP1Channel.Configuration
/// - Parameters:
/// - additionalChannelHandlers: Additional channel handlers to add to channel pipeline after HTTP part decoding and
/// before HTTP request processing
/// - idleTimeout: Time before closing an idle channel
public init(
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = [],
idleTimeout: TimeAmount? = nil
) {
self.additionalChannelHandlers = additionalChannelHandlers
self.idleTimeout = idleTimeout
}
}

/// Initialize HTTP1Channel
/// - Parameters:
/// - responder: Function returning a HTTP response for a HTTP request
/// - additionalChannelHandlers: Additional channel handlers to add to channel pipeline
/// - additionalChannelHandlers: Additional channel handlers to add to channel pipeline after HTTP part decoding and
/// before HTTP request processing
@available(*, deprecated, renamed: "HTTP1Channel(responder:configuration:)")
public init(
responder: @escaping HTTPChannelHandler.Responder,
additionalChannelHandlers: @escaping @Sendable () -> [any RemovableChannelHandler] = { [] }
) {
self.additionalChannelHandlers = additionalChannelHandlers
self.configuration = .init(additionalChannelHandlers: additionalChannelHandlers())
self.responder = responder
}

/// Initialize HTTP1Channel
/// - Parameters:
/// - responder: Function returning a HTTP response for a HTTP request
/// - configuration: HTTP1 channel configuration
public init(
responder: @escaping HTTPChannelHandler.Responder,
configuration: Configuration = .init()
) {
self.configuration = configuration
self.responder = responder
}

Expand All @@ -40,17 +75,18 @@ public struct HTTP1Channel: ServerChildChannel, HTTPChannelHandler {
/// - logger: Logger used during setup
/// - Returns: Object to process input/output on child channel
public func setup(channel: Channel, logger: Logger) -> EventLoopFuture<Value> {
let childChannelHandlers: [any ChannelHandler] =
[HTTP1ToHTTPServerCodec(secure: false)] + self.additionalChannelHandlers() + [
HTTPUserEventHandler(logger: logger),
]
return channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.configureHTTPServerPipeline(
withPipeliningAssistance: false, // HTTP is pipelined by NIOAsyncChannel
withErrorHandling: true,
withOutboundHeaderValidation: false // Swift HTTP Types are already doing this validation
)
try channel.pipeline.syncOperations.addHandlers(childChannelHandlers)
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: false))
try channel.pipeline.syncOperations.addHandlers(self.configuration.additionalChannelHandlers())
if let idleTimeout = self.configuration.idleTimeout {
try channel.pipeline.syncOperations.addHandler(IdleStateHandler(readTimeout: idleTimeout))
}
try channel.pipeline.syncOperations.addHandler(HTTPUserEventHandler(logger: logger))
return try NIOAsyncChannel(
wrappingChannelSynchronously: channel,
configuration: .init()
Expand All @@ -70,7 +106,7 @@ public struct HTTP1Channel: ServerChildChannel, HTTPChannelHandler {
}

public let responder: HTTPChannelHandler.Responder
let additionalChannelHandlers: @Sendable () -> [any RemovableChannelHandler]
public let configuration: Configuration
}

/// Extend NIOAsyncChannel to ServerChildChannelValue so it can be used in a ServerChildChannel
Expand Down
25 changes: 23 additions & 2 deletions Sources/HummingbirdCore/Server/HTTP/HTTPServerBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,34 @@ extension HTTPServerBuilder {
/// server: .http1()
/// )
/// ```
/// - Parameter additionalChannelHandlers: Additional channel handlers to add to channel pipeline
/// - Parameter additionalChannelHandlers: Additional channel handlers to add to channel pipeline after HTTP part decoding and
/// before HTTP request processing
/// - Returns: HTTPServerBuilder builder
@available(*, deprecated, renamed: "http1(configuration:)")
public static func http1(
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = []
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler]
) -> HTTPServerBuilder {
return .init { responder in
return HTTP1Channel(responder: responder, additionalChannelHandlers: additionalChannelHandlers)
}
}

/// Return a `HTTPServerBuilder` that will build a HTTP1 server
///
/// Use in ``Hummingbird/Application`` initialization.
/// ```
/// let app = Application(
/// router: router,
/// server: .http1(configuration: .init(idleTimeout: .seconds(30)))
/// )
/// ```
/// - Parameter configuration: HTTP1 channel configuration
/// - Returns: HTTPServerBuilder builder
public static func http1(
configuration: HTTP1Channel.Configuration = .init()
) -> HTTPServerBuilder {
return .init { responder in
return HTTP1Channel(responder: responder, configuration: configuration)
}
}
}
82 changes: 52 additions & 30 deletions Sources/HummingbirdHTTP2/HTTP2Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,57 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
public let channel: Channel
}

/// HTTP2 Upgrade configuration
public struct Configuration: Sendable {
/// Configuration applied to HTTP2 stream channels
public var streamConfiguration: HTTP1Channel.Configuration

/// Initialize HTTP2UpgradeChannel.Configuration
/// - Parameters:
/// - additionalChannelHandlers: Additional channel handlers to add to HTTP2 connection channel
/// - streamConfiguration: Configuration applied to HTTP2 stream channels
public init(
streamConfiguration: HTTP1Channel.Configuration = .init()
) {
self.streamConfiguration = streamConfiguration
}
}

private let sslContext: NIOSSLContext
private let http1: HTTP1Channel
private let additionalChannelHandlers: @Sendable () -> [any RemovableChannelHandler]
public var responder: HTTPChannelHandler.Responder { self.http1.responder }

/// Initialize HTTP1Channel
/// Initialize HTTP2Channel
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - additionalChannelHandlers: Additional channel handlers to add to channel pipeline
/// - responder: Function returning a HTTP response for a HTTP request
@available(*, deprecated, renamed: "HTTP1Channel(tlsConfiguration:configuration:responder:)")
public init(
tlsConfiguration: TLSConfiguration,
additionalChannelHandlers: @escaping @Sendable () -> [any RemovableChannelHandler] = { [] },
additionalChannelHandlers: @escaping @Sendable () -> [any RemovableChannelHandler],
responder: @escaping HTTPChannelHandler.Responder
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.additionalChannelHandlers = additionalChannelHandlers
self.http1 = HTTP1Channel(responder: responder, additionalChannelHandlers: additionalChannelHandlers)
self.http1 = HTTP1Channel(responder: responder, configuration: .init(additionalChannelHandlers: additionalChannelHandlers()))
}

/// Initialize HTTP2Channel
/// - Parameters:
/// - tlsConfiguration: TLS configuration
/// - additionalChannelHandlers: Additional channel handlers to add to channel pipeline
/// - responder: Function returning a HTTP response for a HTTP request
public init(
tlsConfiguration: TLSConfiguration,
configuration: Configuration = .init(),
responder: @escaping HTTPChannelHandler.Responder
) throws {
var tlsConfiguration = tlsConfiguration
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols
self.sslContext = try NIOSSLContext(configuration: tlsConfiguration)
self.http1 = HTTP1Channel(responder: responder, configuration: configuration.streamConfiguration)
}

/// Setup child channel for HTTP1 with HTTP2 upgrade
Expand All @@ -65,38 +96,29 @@ public struct HTTP2UpgradeChannel: HTTPChannelHandler {
}

return channel.configureAsyncHTTPServerPipeline { http1Channel -> EventLoopFuture<HTTP1Channel.Value> in
let childChannelHandlers: [ChannelHandler] =
[HTTP1ToHTTPServerCodec(secure: false)] +
self.additionalChannelHandlers() +
[HTTPUserEventHandler(logger: logger)]

return http1Channel
.pipeline
.addHandlers(childChannelHandlers)
.flatMapThrowing {
try HTTP1Channel.Value(wrappingChannelSynchronously: http1Channel)
return http1Channel.eventLoop.makeCompletedFuture {
try http1Channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))
try http1Channel.pipeline.syncOperations.addHandlers(self.http1.configuration.additionalChannelHandlers())
if let idleTimeout = self.http1.configuration.idleTimeout {
try http1Channel.pipeline.syncOperations.addHandler(IdleStateHandler(readTimeout: idleTimeout))
}
try http1Channel.pipeline.syncOperations.addHandler(HTTPUserEventHandler(logger: logger))
return try HTTP1Channel.Value(wrappingChannelSynchronously: http1Channel)
}
} http2ConnectionInitializer: { http2Channel -> EventLoopFuture<NIOAsyncChannel<HTTP2Frame, HTTP2Frame>> in
http2Channel.eventLoop.makeCompletedFuture {
try NIOAsyncChannel<HTTP2Frame, HTTP2Frame>(wrappingChannelSynchronously: http2Channel)
}
} http2StreamInitializer: { http2ChildChannel -> EventLoopFuture<HTTP1Channel.Value> in
let childChannelHandlers: NIOLoopBound<[ChannelHandler]> =
.init(
self.additionalChannelHandlers() + [
HTTPUserEventHandler(logger: logger),
],
eventLoop: channel.eventLoop
)

return http2ChildChannel
.pipeline
.addHandler(HTTP2FramePayloadToHTTPServerCodec())
.flatMap {
http2ChildChannel.pipeline.addHandlers(childChannelHandlers.value)
}.flatMapThrowing {
try HTTP1Channel.Value(wrappingChannelSynchronously: http2ChildChannel)
return http2ChildChannel.eventLoop.makeCompletedFuture {
try http2ChildChannel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTPServerCodec())
try http2ChildChannel.pipeline.syncOperations.addHandlers(self.http1.configuration.additionalChannelHandlers())
if let idleTimeout = self.http1.configuration.idleTimeout {
try http2ChildChannel.pipeline.syncOperations.addHandler(IdleStateHandler(readTimeout: idleTimeout))
}
try http2ChildChannel.pipeline.syncOperations.addHandler(HTTPUserEventHandler(logger: logger))
return try HTTP1Channel.Value(wrappingChannelSynchronously: http2ChildChannel)
}
}.map {
.init(negotiatedHTTPVersion: $0, channel: channel)
}
Expand Down
28 changes: 27 additions & 1 deletion Sources/HummingbirdHTTP2/HTTPServerBuilder+http2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ extension HTTPServerBuilder {
/// - tlsConfiguration: TLS configuration
/// - additionalChannelHandlers: Additional channel handlers to call before handling HTTP
/// - Returns: HTTPChannelHandler builder
@available(*, deprecated, renamed: "http2Upgrade(tlsConfiguration:configuration:)")
public static func http2Upgrade(
tlsConfiguration: TLSConfiguration,
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler] = []
additionalChannelHandlers: @autoclosure @escaping @Sendable () -> [any RemovableChannelHandler]
) throws -> HTTPServerBuilder {
return .init { responder in
return try HTTP2UpgradeChannel(
Expand All @@ -42,4 +43,29 @@ extension HTTPServerBuilder {
)
}
}

/// Build HTTP channel with HTTP2 upgrade
///
/// Use in ``Hummingbird/Application`` initialization.
/// ```
/// let app = Application(
/// router: router,
/// server: .http2Upgrade(configuration: .init(tlsConfiguration: tlsConfiguration))
/// )
/// ```
/// - Parameters:
/// - configuration: HTTP2 Upgrade channel configuration
/// - Returns: HTTPChannelHandler builder
public static func http2Upgrade(
tlsConfiguration: TLSConfiguration,
configuration: HTTP2UpgradeChannel.Configuration = .init()
) throws -> HTTPServerBuilder {
return .init { responder in
return try HTTP2UpgradeChannel(
tlsConfiguration: tlsConfiguration,
configuration: configuration,
responder: responder
)
}
}
}
30 changes: 16 additions & 14 deletions Tests/HummingbirdCoreTests/CoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ final class HummingBirdCoreTests: XCTestCase {
try await bodyWriter.write(request.body.delayed())
try await bodyWriter.finish(nil)
},
httpChannelSetup: .http1(additionalChannelHandlers: [SlowInputChannelHandler()]),
httpChannelSetup: .http1(configuration: .init(additionalChannelHandlers: [SlowInputChannelHandler()])),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
Expand Down Expand Up @@ -313,7 +313,7 @@ final class HummingBirdCoreTests: XCTestCase {
}
try await responseWriter.writeResponse(.init(status: .ok))
},
httpChannelSetup: .http1(additionalChannelHandlers: [CreateErrorHandler()]),
httpChannelSetup: .http1(configuration: .init(additionalChannelHandlers: [CreateErrorHandler()])),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
Expand Down Expand Up @@ -384,10 +384,10 @@ final class HummingBirdCoreTests: XCTestCase {
}
try await responseWriter.writeResponse(.init(status: .ok))
},
httpChannelSetup: .http1(additionalChannelHandlers: [
HTTPServerIncompleteRequest(),
IdleStateHandler(readTimeout: .seconds(1)),
]),
httpChannelSetup: .http1(configuration: .init(
additionalChannelHandlers: [HTTPServerIncompleteRequest()],
idleTimeout: .seconds(1)
)),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
Expand Down Expand Up @@ -422,10 +422,10 @@ final class HummingBirdCoreTests: XCTestCase {
}
try await responseWriter.writeResponse(.init(status: .ok))
},
httpChannelSetup: .http1(additionalChannelHandlers: [
HTTPServerIncompleteRequest(),
IdleStateHandler(readTimeout: .seconds(1)),
]),
httpChannelSetup: .http1(configuration: .init(
additionalChannelHandlers: [HTTPServerIncompleteRequest()],
idleTimeout: .seconds(1)
)),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
Expand Down Expand Up @@ -468,10 +468,12 @@ final class HummingBirdCoreTests: XCTestCase {
}
try await responseWriter.writeResponse(.init(status: .ok))
},
httpChannelSetup: .http1(additionalChannelHandlers: [
HTTPServerIncompleteRequest(),
IdleStateHandler(readTimeout: .seconds(1)),
]),
httpChannelSetup: .http1(
configuration: .init(
additionalChannelHandlers: [HTTPServerIncompleteRequest()],
idleTimeout: .seconds(1)
)
),
configuration: .init(address: .hostname(port: 0)),
eventLoopGroup: Self.eventLoopGroup,
logger: Logger(label: "Hummingbird")
Expand Down

0 comments on commit 61a7d64

Please sign in to comment.