Two things I am trying to do in Swift NIO feel a little trickier than I expect:
An HTTPS (indeed, HTTP/2) pipeline which redirects clients with 301 Moved Permanently when accessed over HTTTP.
Current Solution: the first two handlers in the pipeline are NIOSSLServerHandler and a custom HTTPTrampoline handler which implements errorCaught. If the error is a handshake error, HTTPTrampoline removes the SSL handler and writes a canned message like so:
context.pipeline.handler(type: NIOSSLHandler.self).whenSuccess { handler in
context.pipeline.removeHandler(handler).whenSuccess { _ in
context.writeAndFlush(outboundOut).whenComplete { _ in
context.close(promise: nil)
}
}
}
An HTTP/2 pipeline which mostly uses HTTP2StreamMultiplexer but also allows things like server push.
Current Solution: I haven't implemented this yet, but my idea is to build the pipeline how the NIOHTTP2Server example is built, but for server push use context.pipeline.handler(type: HTTP2StreamMultiplexer.self) to create a new stream and context.pipeline.context(handler: http2StreamMultiplexer) and to manually send a .pushPromise frame.
As I mentioned, both of these seem overly complex for what they enable, and that makes me wonder:
Am I missing a much easier way to do this? I imagine nonlinear or branching ChannelPipelines are not uncommon.
Why did NIO select a type-erased representation rather something more explicit (like NIOSSLHandler<HTTP2StreamMultiplexer<HTTP2toHTTP1ServerCodec<MyHandler>>>? Admittedly that is a little nasty but now with some ChannelHandler this could be drastically improved.
Branching ChannelPipelines are very common, both in terms of pipelines that branch only once (I.e. are setup differently depending on the content they receive) and those that branch forever (I.e. deliver data to different sub-Channels depending on the content.
For your first idea, I’d like to begin by cautioning that you can avoid the need for this pipeline entirely by simply running HTTPS and HTTP on separate ports (443 and 80 being the common ones). In general, running HTTPS and HTTP on the same port is an anti-pattern.
However, if you want to do it, the better move is to insert a handler that does content-detection on the data. The most common move is to have a handler that looks for the initial bytes of a TLS Client Hello, which are always the same, and if it doesn’t see that then it assumes the request is plaintext HTTP. The ChannelHandler waits for the bytes to be read, and then calls one of two closures to set the pipeline up. This is very similar to the interface to ApplicationLayerProtocolNegotiationHandler in NIOTLS.
For solution 2, there’s no need to do that. Simply use the HTTP2StreamMultiplexer to create a new stream and then call channel.write(pushPromiseFrame)on that new stream channel to send the push promise frame. In general, reaching into the context of the stream multiplexer is a bad idea and should be avoided.
As to why we selected a type erased representation, the reason is that we were operating in a more restricted version of the language. Going forward we will be investigating opportunities to improve the type safety of the system.
I see, so it looks like setup closures (similar to HTTP2StreamMultiplexer as well) are the current accepted way to do branching pipelines. Any thoughts about passing the stream multiplexer as an argument to the closure (so the downstream pipeline may use pushPromise) versus using context.pipeline.handler(type: HTTP2StreamMultiplexer.self)?
A small aside: I agree about HTTP and HTTPS on separate points, but if the plan is to only ever return 301 Moved Permanently from HTTP I think the anti pattern is OK, and it has the added benefit of when I type "localhost:8080" into a browser during development, it does the right thing.
The HTTP2StreamMultiplexer is currently passed to a closure if you use channel.configureHTTP2Pipeline(), which is the recommended way to add the HTTP2Handlers to a ChannelPipeline. So the prior art is with you there.