How to upgrade an HTTP/1 server to support HTTP/2?

i suppose if server-side swift were easy, everyone would do it…

i’ve currently got an HTTP/1-only server whose ServerBootstrap does something like:

guard let tls:NIOSSLContext = ...
else
{
    //  No HTTPS
    return channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true)
        .flatMap
    {
        channel.pipeline.addHandler(endpoint)
    }
}
return  channel.pipeline.addHandler(NIOSSLServerHandler.init(context: tls))
        .flatMap
{
    //  HTTPS
        channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true)
        .flatMap
    {
        channel.pipeline.addHandler(endpoint)
    }
}

the endpoint is a channel handler with:

typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart

this works fine.

after wandering aimlessly through the documentation for the two packages involved (both on the swift package index and my own index site), i deduced that Channel.configureCommonHTTPServerPipeline(h2ConnectionChannelConfigurator:_:) is what i needed, and i changed the previous code to the following:

guard let tls:NIOSSLContext = authority.tls as? NIOSSLContext
else
{
    return channel.configureCommonHTTPServerPipeline
    {
        $0.pipeline.addHandler(endpoint)
    }
}
return  channel.pipeline.addHandler(NIOSSLServerHandler.init(context: tls))
        .flatMap
{
        channel.configureCommonHTTPServerPipeline
    {
        $0.pipeline.addHandler(endpoint)
    }
}

sadly, this doesn’t “just work”, the new channel doesn’t respond to anything anymore. what am i doing wrong?

it turns out that HTTP/2 requires (and is negotiated over) TLS, and you need to add HTTP/2 to the list of acceptable protocols in NIOSSLContext.

posting here in case someone else stumbles upon this thread:

var configuration:TLSConfiguration = .makeServerConfiguration(
    certificateChain: certificates.map(NIOSSLCertificateSource.certificate(_:)),
    privateKey: .privateKey(privateKey))

    configuration.applicationProtocols = ["h2", "http/1.1"]

let niossl:NIOSSLContext = try .init(configuration: configuration)
1 Like

Correct. If you want to setup a pipeline that supports both HTTP/1.1 and HTTP/2 then something has to do the protocol negotiation for you. This is mostly done via TLS as you discovered. RFC 9113 also explains how to start an HTTP/2 connection with prior knowledge.

If you are up for the latest async APIs that we are working on, this setups a connection that handles both HTTP/1.1 and HTTP/2 and provides you our new NIOAsyncChannel based interfaces to handle incoming connections. (Omitted the sslContext creation)

let channel = try await ServerBootstrap(group: eventLoopGroup)
    .bind(host: "127.0.0.1", port: 8080) { channel in
        return channel.pipeline.addHandler(NIOSSLServerHandler(context: sslContext))
            .flatMap {
                channel.configureAsyncHTTPServerPipeline { http1ConnectionChannel in
                    http1ConnectionChannel.eventLoop.makeCompletedFuture {
                        try http1ConnectionChannel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: false))
                        return try NIOAsyncChannel(
                            synchronouslyWrapping: http1ConnectionChannel,
                            configuration: .init(
                                inboundType: HTTPTypeRequestPart.self,
                                outboundType: HTTPTypeResponsePart.self
                            )
                        )
                    }
                } http2ConnectionInitializer: { http2ConnectionChannel in
                    http2ConnectionChannel.eventLoop.makeCompletedFuture {
                        try NIOAsyncChannel(
                            synchronouslyWrapping: http2ConnectionChannel,
                            configuration: .init(
                                inboundType: HTTP2Frame.self,
                                outboundType: HTTP2Frame.self
                            )
                        )
                    }
                } http2InboundStreamInitializer: { http2StreamChannel in
                    http2StreamChannel.eventLoop.makeCompletedFuture {
                        try http2StreamChannel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTPServerCodec())
                        return try NIOAsyncChannel(
                            synchronouslyWrapping: http2StreamChannel,
                            configuration: .init(
                                inboundType: HTTPTypeRequestPart.self,
                                outboundType: HTTPTypeResponsePart.self
                            )
                        )
                    }
                }
            }
    }
1 Like

i’m sure i’m probably missing something obvious, but i can’t seem to find any of those APIs, e.g. configureAsyncHTTPServerPipeline.

ironically, a google search for “swift-nio configureAsyncHTTPServerPipeline” directs me back to this thread, which has apparently already become the top search result in the 21 hours since i posted this :slight_smile:


EDIT: found it, it looks like it is main-branch only (1.27.0, main). any chance we can get a release tagged, so i can use it in SPM?