Requirements for a High Level HTTP Server type

Hi all,

In a recent Swift Server Work Group (SSWG) meeting we discussed the possibility of creating a shared High Level HTTP Server type. The aim of this post is to socialise this proposal and gather the requirements for such a type.

Conceptually such a type would fit into the High-level implementations matrix on SwiftNIO's Github README where there is currently an X for a HTTP Server. It would have a similar aim to the other implementations in this matrix of providing an API that doesn't expose SwiftNIO's Channel related types. As this matrix would suggest, this High Level HTTP Server type would form a counterpart to swift-server/async-http-client.

The primary audience for this proposed type would be a Server Side developer who wants to start with a http server and build from there. Currently the starting point for this use case is not ideal - either pulling in an entire framework to use its existing http server or getting down into SwiftNIO to understand the complexities of building an optimised and robust server.

Looking at existing http server implementations - including those exploring some of the recent Swift-concurrency bridges being developed by the SwiftNIO team - we believe there is sufficient commonality between the requirements of different use cases that a shared implementation would be possible and that this logic is sufficiently complex to warrant a shared type.

Having a well-socialised shared High Level HTTP Server type that demonstrates the best practice patterns for managing SwiftNIO's Channels should provide a more natural starting point for these use cases, allowing developers to implement their custom business logic quickly and with confidence. This is also important because this experience might be the first a developer has of the Swift Server Side ecosystem.

The goal here though is not to build an even higher web framework that comes with mechanisms such as routing, middleware (which the SSWG is tracking as a separate initiative), request/response (de)serialisation/manipulation, authorisation, error handling (outside of that related to the channel itself) or HTTP header handling such as session or cookie management. The Swift ecosystem already has solutions for these use cases.

Finally, we believe understanding the requirements of such a shared type are more important than specific implementation details at this point so that is what we are hoping to focus on in this thread.

So after all that, please share requirements that you think a shared High Level HTTP Server type should fulfil!

26 Likes

Thanks for opening the discussion @tachyonics. Really looking forward to what the rest of the community thinks about this and what everyone envisions such a type provides.

Here are a couple of requirements that come to my mind:

  • Supports H1/H2 (potentially H3 in the future)
  • Supports TLS and non-TLS
  • Supports connection re-use
  • Supports informational status responses (status codes 10X)
  • Supports fully bi-directional streaming of requests and responses with back pressure in both directions
  • Leverages Concurrency and Structured Concurrency in its APIs

Some configuration options that we should consider supporting:

  • Request timeouts
  • TCP keep alive timeouts
  • Maximum headers
  • Maximum concurrent requests

Some things that are not requirements per-se but we should keep in mind:

  • Easy to use API (We should look at NodeJS and Go provide)
  • High throughput (This type should be attractive to use for even high throughput applications)
13 Likes

If you are interested in Node+Swift, I have two implementations of this, the older GCD based Noze.io and the newer NIO based Macro.swift.
The strength of Node IMO is the streaming system (which Macro only implements partially, Noze is reimplementing it more completely on top of GCD). Not sure how channels and such would fit into this.

5 Likes

I think @FranzBusch has covered most of what I would consider requirements for a modern HTTP server written in Swift.

In my view there are a couple of things missing though.

  • Support for pipeline upgrades such as WebSocket or gRPC. I don't expect these to be implemented but I do expect to be given the ability to tear down the HTTP pipeline and replace with a new pipeline based off an upgrade request. Once the HTTP server is implemented maybe we could look at a generic WebSocket server implementation, but that's another discussion.
  • Low level support for custom Channel initialisation which allows for custom HTTP pipelines.
    • This could be used to aid implementation of the previous point.
    • It will allow us to modularise HTTP features. An HTTP2 upgrade feature could be placed in a separate target/library if the Channel setup can be separated from the core server.
    • It allows for support of custom pipelines specific to individual projects.

Other configuration settings may include

  • Max request payload size (although this could possibly be done at a higher level)
  • Idle channel timeouts times

While I prefer an easy to use API like @FranzBusch mentioned, I have to add that for a package on this level I really don't like to see it as a primary goal. Frameworks building on top of this should be able to consume the exposed APIs provided by this server, and vend their own flavour towards their end users.

  • It has to lean on a standardised set of HTTP types, which are practical for frameworks to expose 1:1, or wrap into their own helpers.
  • Collecting the body into a big ByteBuffer is a big no, first of all.
    • I think that exposing incoming bodies an AsyncSequence of ByteBuffer is necessary here.

A nice to have would be providing a set of helpers for processing, for example, Chunked Transofer Encoding. Possibly in an -extras package/module.

If reasonably achievable in 1.0, I'd definitely be interested in keeping the system open to a future HTTP/3 implementation.

2 Likes

A huge problem with modelling this as teardown is that it prevents us from supporting protocols that don't model their upgrade this way. For example, Websocket in H2 is modelled within a H2 stream. I agree that we should find a way to support this kind of use-case, but I don't think we want to model it exactly this way.

1 Like

Another thing that we should look at is resumable HTTP uploads/downloads which were just mentioned in a WWDC talk

2 Likes

mind elaborating why? i personally have not had a lot of success doing “streaming” things directly with sequences of ByteBuffer, this seems like something better handled with something like ByteToMessageDecoder.

In general, the new HTTPServer should expose its fundamental request handler APIs in terms of an AsyncSequence of incoming bytes. It must do this since streaming is a fundamental thing in the various HTTP specs; however, a very common thing is to collect the whole body and decode a JSON from it. We should make this as easy as possible. In the AsyncHTTPClient our body sequences have a collect(upTo:) method which is aimed at exactly that use-case.

2 Likes

Franz covered most of it, I'll just call out a few specific configurations that might not be very common, but are important to support:

  • HTTP/2 without TLS, required in environments where a secure socket proxy is used
  • limiting total memory used by unconsumed request body chunks, which is related to the back-pressure requirement that Franz mentioned, but often you don't want to limit the memory consumed per-request, but rather per-server, to allow maximum use of memory without going over the memory limit

It's possible that some of the features mentioned would not be implemented in this new server package, but on top of it, which is fine, we just need to make sure that the new API doesn't prevent this level of control to be wielded by a package built on top.

1 Like

I think this is a great initiative and something Vapor would be very interested in! Having a shared layer between NIO and would provide a solid foundation whilst taking some of the pain points away.

I agree with much of what has been said above, and want to echo:

  • A good API - making it easy to integrate would be extremely helpful, but I'm happy to take some pain for additional features
  • An extensible API - making it easy to adopt new use cases without breaking choices both from the library point of view and adopting libraries point of view
  • Make it fully integrated with Swift Concurrency - exposing AsyncSequences is a must, things being Sendable is a must where applicable etc
  • Streaming by default, support for H1/H2/H3, gRPC/Websockets (or at least hooks to do so), SSEs
3 Likes

That's really great, the current usability of NIO is too low, I need to learn more deeply in order to apply it well to my projects.Currently, like swift-server/async-http-client, SwiftNIO , I need to pay special attention to eventLoopGroupProvider , which sometimes confuses me, why can't it be as convenient as URLSession ?

URLSession uses a bunch of global singletons:

  1. A global singleton connection pool
  2. Dispatch's global singleton thread pool
  3. a singleton URLSession instance at URLSession.shared which uses the above two

Libraries like SwiftNIO aren't/shouldn't normally in the business of creating globally singletons because that can be done in a higher-level library. Vapor is a great example, their Application just creates the EventLoopGroup and hands it to the HTTP client without the user noticing. Perfect.

But as things panned out in the Swift on Server ecosystem, there isn't really another library on top of SwiftNIO that everything shares that could host these singletons. That's why SwiftNIO has literally yesterday taken the step to actually host a singleton EventLoopGroup for everybody: https://github.com/apple/swift-nio/pull/2471 . Having global singletons isn't great but confusing the community even less so.

AsyncHTTPClient will be adopting this as soon as it's released: use NIOSingletons EventLoops/NIOThreadPool instead of spawning new by weissi · Pull Request #697 · swift-server/async-http-client · GitHub

Once that's done, you can just do HTTPClient(eventLoopGroup: .singleton) and call it a day. And as a followup AsyncHTTPClient could then itself do the same thing and just provide an

private sharedSingleton = HTTPClient(eventLoopGroupProvider: .singleton)

extension HTTPClient {
    public var shared: HTTPClient {
        return sharedSingleton
    }
}

which would make it as convenient as URLSession. Filed this as feature request: HTTPClient.shared (like URLSession.shared)? · Issue #700 · swift-server/async-http-client · GitHub

1 Like

I really like this proposal. It would be very helpful for my simple programs on Linux (non-HTTP servers). It greatly simplifies thread maintenance for me.