Designing an HTTP Server API for Swift

Swift is becoming increasingly popular for server-side development, thanks to characteristics such as performance, safety, and developer productivity. At Apple, we’re increasingly writing new services in Swift and are therefore continuing to invest in improving its approachability for developing server-side applications. As a part of the ongoing work in the proposed Swift networking vision, we invite your participation as we work together to make Swift the best language for all networking use cases.

Existing frameworks provide solutions for building web applications, but they have opted to build their own low-level HTTP server implementation. This leads to a few issues:

  • Duplicated implementations using the same building blocks (i.e. SwiftNIO), each with potentially different bugs, security or performance issues to deal with.
  • Lack of unified currency types or abstractions, making it hard to share code across implementations.
  • Some implementations lack structured concurrency support, forcing developers to use awkward adapters to bridge future- or callback-based APIs into async/await code.
  • Bidirectional streaming support is inconsistent across implementations, particularly around HTTP trailers and request lifecycle management. This creates challenges for features that need visibility into the full request lifetime, such as distributed tracing via middleware or interceptors.
  • Applications with simple networking needs currently face an awkward choice: use a full-featured framework like Vapor and accept its large dependency footprint, or work directly with SwiftNIO's low-level APIs. An intermediate layer would fill this gap, offering straightforward networking capabilities with minimal dependencies and appropriate abstraction.

An ideal solution would be to provide a low-level HTTP server that addresses these problems by providing a structured, concurrency-first approach that other frameworks can build upon, with full bidirectional streaming support, structured resource management, and safe APIs. Today we’re sharing an API and prototype implementation that demonstrates early progress towards this goal.

Goals

We’ve got two main, high-level goals as part of this effort:

  • Provide an abstract API interface, to be used by users who may want to build on top of the HTTP Server in an implementation-agnostic way (primarily library developers).
  • Provide a high-performance concrete implementation of this interface based on SwiftNIO that can be used out-of-the-box.

For both pieces, we want full support for structured concurrency and resource management. This would enable in turn support for middlewares and bidirectional streaming, including trailers. This also would include the ability to capture the whole lifecycle of a streaming request and response (including their trailers), for things like tracing interceptors.

For the abstract interface in particular, our main goal is to provide an API that makes it hard for users to make mistakes, and uses ecosystem-wide currency types (such as swift-http-types).

For the concrete NIO implementation, we have a few more specific goals:

  • HTTP/1.1, HTTP/2 and HTTP/3 support, with protocol negotiation, (m)TLS and certificate rotation.
  • Support for graceful shutdown.
  • APIs for connection management configuration (maximum number of connections, idle timeout, etc).
  • Backpressure support:
    • On the number of requests a client can make
    • When reading from request bodies
    • When writing response bodies

Note: there is an HTTPClient counterpart to this project that you can find linked at the bottom of this post.

Proposal

This API is still in its infancy and everyone is very much encouraged to provide feedback to make it the best it can be.

As we mentioned, this project has two parts: the abstract interface made up of several protocols that will allow greater flexibility for developers to build on top of HTTP server abstractions without being tied to any specific networking stack; and a concrete implementation of these protocols, backed by SwiftNIO.

Abstract interface

HTTPServer

An abstract HTTPServer protocol that defines a single serve(handler:) method. Concrete types conforming to this protocol will provide this single, long-running method that will handle all incoming requests.

The protocol itself is straightforward:

public protocol HTTPServer: Sendable, ~Copyable, ~Escapable {
    associatedtype RequestConcludingReader: ConcludingAsyncReader
    associatedtype ResponseConcludingWriter: ConcludingAsyncWriter

    func serve(handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>) async throws
}

The protocol is marked as ~Copyable and ~Escapable to support modern Swift ownership semantics, ensuring efficient resource management. The single serve(handler:) method takes a request handler and runs indefinitely, processing all incoming HTTP connections.

To use an HTTPServer, you create a handler that conforms to HTTPServerRequestHandler and pass it to the server's serve method:

struct EchoHandler: HTTPServerRequestHandler {
    func handle(
        request: HTTPRequest,
        requestContext: HTTPRequestContext,
        requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
        responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
    ) async throws {
        let response = HTTPResponse(status: .ok)
        let writer = try await responseSender.send(response)
    }
}

We will dive deeper into the request handler below. Once you have your handler, you instantiate a concrete server implementation and start it:

let server = ... // create a concrete HTTPServer instance
try await server.serve(handler: EchoHandler())

This design keeps the server protocol minimal while delegating all request-handling logic to the handler, allowing for clean separation of concerns.

HTTPServerRequestHandler

The handler is an HTTPServerRequestHandler: a protocol that defines how a request should be handled. This protocol fully supports bidirectional streaming HTTP request handling, including optional request and response trailers.

The protocol definition currently looks like this:

public protocol HTTPServerRequestHandler<RequestReader, ResponseWriter>: Sendable {
    associatedtype RequestReader: ConcludingAsyncReader
    associatedtype ResponseWriter: ConcludingAsyncWriter

    func handle(
        request: HTTPRequest,
        requestContext: HTTPRequestContext,
        requestBodyAndTrailers: consuming sending RequestReader,
        responseSender: consuming sending HTTPResponseSender<ResponseWriter>
    ) async throws
}

The protocol uses two associated types that define how request bodies are read and response bodies are written. This gives HTTP Server implementations (and their users) the flexibility to provide custom readers and writers that fit their use cases, while keeping handler implementations agnostic to be used.

(Note: Having these associated types basically requires all HTTPServerRequestHandler implementations to be generic. Otherwise, if handler implementations would decide which concrete reader and writer types to use, that would constrain which servers they can be used with. Alternatives, such as moving the generics to the handle method, are currently being considered and you’re welcome to participate in the discussions.)

The reader and writer conform the ConcludingAsyncReader and ConcludingAsyncWriter types respectively, which enable streaming with a final element. In this case, the stream element type is UInt8, and the final element is and optional HTTPFields (from swift-http-types) representing trailers. These types are defined in the AsyncStreaming module defined in the swift-http-api-proposal repository, and they’re described in a bit more detail in the HTTPRequestConcludingAsyncReader and HTTPResponseConcludingAsyncWriter sections below.

When implementing handle(request:requestContext:requestBodyAndTrailers:responseSender:), your handler receives four parameters:

  1. request - The HTTP request headers and metadata
  2. requestContext - Additional context about the request
  3. requestBodyAndTrailers - A reader for streaming the request body and receiving trailers
  4. responseSender - A mechanism for sending back responses

Here's how these pieces work together in a streaming echo handler that reads request data and writes it back.
Let’s define a new EchoHandler that will echo back requests.

struct EchoHandler<
    ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable & SendableMetatype,
    RequestReader: AsyncReader<UInt8, any Error> & ~Copyable,
    ConcludingResponseWriter: ConcludingAsyncWriter<ResponseWriter, HTTPFields?> & ~Copyable & SendableMetatype,
    ResponseWriter: AsyncWriter<UInt8, any Error> & ~Copyable
>: HTTPServerRequestHandler {
  func handle(
    request: HTTPRequest,
    requestContext: HTTPRequestContext,
    requestBodyAndTrailers: consuming sending ConcludingRequestReader,
    responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
  ) async throws {
    // We'll do this in a bit...
  }
}

The echo handler begins by wrapping the request async reader in an optional so we can consume it within the response writing closure. This is currently necessary because Swift currently lacks call-once closures:

var requestBodyAndTrailers = Optional(requestBodyAndTrailers)

We next send back a response with a 202 Accepted status. This consumes the response sender and returns a writer for the response body.

let responseBodyAndTrailers = try await responseSender.send(.init(status: .accepted))

Next, we use the response writer to echo back the request data, reading from the request reader and writing each chunk to the response.

try await responseBodyAndTrailers.produceAndConclude { writer in
  var writer = writer
  return try await requestBodyAndTrailers.take()!.consumeAndConclude { reader in
    try await reader.forEach { span in
      try await writer.write(span)
    }
  }
}

All in all, this is what the handler looks like:

func handle(
  request: HTTPRequest,
  requestContext: HTTPRequestContext,
  requestBodyAndTrailers: consuming sending ConcludingRequestReader,
  responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
) async throws {
  var requestBodyAndTrailers = Optional(requestBodyAndTrailers)
  let responseBodyAndTrailers = try await responseSender.send(.init(status: .accepted))
  try await responseBodyAndTrailers.produceAndConclude { writer in
    var writer = writer
    return try await requestBodyAndTrailers.take()!.consumeAndConclude { reader in
      try await reader.forEach { span in
        try await writer.write(span)
      }
    }
  }
}

This pattern ensures proper resource management through Swift's ownership system - the consuming and sending keywords guarantee that resources are used exactly once and transferred safely across concurrency boundaries.
We also ensure at compile time that we only send a single response status back, followed by an optional body and trailers.

HTTPServerClosureRequestHandler

For simpler use cases, you don't always want to define a full conforming type. The HTTPServerClosureRequestHandler provides a convenient way to create request handlers using inline closures instead of defining a custom type that conforms to HTTPServerRequestHandler.

Its structure is straightforward: it just takes a closure with HTTPServerRequestHandler/handle’s same signature.
An extension on HTTPServer provides an overload of serve that takes an HTTPServerClosureRequestHandler.

As a simple example, a server providing a Hello World service can be written in just a few lines of code:

let server = ... // create a concrete HTTPServer instance

try await server.serve { request, context, bodyReader, responseSender in
  let writer = try await responseSender.send(HTTPResponse(status: .ok))
  try await writer.writeAndConclude("Hello World".utf8.span, finalElement: nil)
}

This approach is perfect for simple servers, prototyping, or cases where you don't need the structure of a full conforming type.

HTTPResponseSender

The HTTPResponseSender is a critical piece of the API design that enforces proper HTTP response structure through Swift's type system. It's a non-copyable type that ensures responses are sent correctly and prevents common mistakes.

public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable {
  public init(
      send: @escaping (HTTPResponse) async throws -> ResponseWriter,
      sendInformational: @escaping (HTTPResponse) async throws -> Void
  )

  consuming public func send(_ response: HTTPResponse) async throws -> ResponseWriter

  public func sendInformational(_ response: HTTPResponse) async throws
}

@available(*, unavailable)
extension HTTPResponseSender: Sendable {}

This type enforces the HTTP protocol's response flow through its API design:

Informational responses (1xx status codes) can be sent zero or more times using sendInformational(_:). This method is not consuming, so you can call it multiple times. For example, you might send a 100 Continue response before processing a large upload:

try await responseSender.sendInformational(HTTPResponse(status: .continue))

Final responses must be sent exactly once by calling send(_:). ** This method is consuming, which means it takes ownership of the response sender and you cannot use it again. When you call send(_``:), you get back a ResponseWriter:

let writer = try await responseSender.send(HTTPResponse(status: .ok)) 

After calling send(_:), the responseSender is consumed and cannot be referenced again. This is enforced by the compiler, so you cannot send multiple final responses or send them in the wrong order.
Once you have the ResponseWriter, you can write back the response body and optionally include trailers.

This design prevents common HTTP server bugs:

  • No duplicate responses: The consuming keyword makes it impossible to send two final responses
  • Correct ordering: You can't send a final response before informational ones are complete
  • Resource safety: The type system ensures you properly complete the response lifecycle
  • Clear structure: The API guides you toward the correct HTTP response pattern

For example, this code won't compile because it tries to use the response sender twice:

// This won't compile!
let writer1 = try await responseSender.send(HTTPResponse(status: .ok))
let writer2 = try await responseSender.send(HTTPResponse(status: .notFound)) // Error: responseSender was consumed

This compile-time safety is a key advantage of using non-copyable types - the API makes it impossible to misuse, rather than relying on runtime checks or documentation.

Concrete NIO implementation

NIOHTTPServer

This is the concrete NIO-backed implementation for the HTTPServer protocol. It uses its own reader and writer types (HTTPRequestConcludingAsyncReader and HTTPResponseConcludingAsyncWriter respectively), described below.

public struct NIOHTTPServer: HTTPServer {
  public typealias RequestReader = HTTPRequestConcludingAsyncReader
  public typealias ResponseWriter = HTTPResponseConcludingAsyncWriter
  // ...
}

As a simple example, if a user wants to create an instance of a NIOHTTPServer that runs on localhost port 12345 and uses TLS, they can do so as follows:

let server = NIOHTTPServer(
    logger: logger,
    configuration: NIOHTTPServerConfiguration(
        bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
        transportSecurity: .tls(
            certificateChain: /* some cert chain */,
            privateKey: /* some private key */
        )
    )
)

try await server.serve(handler: EchoHandler())

HTTPRequestConcludingAsyncReader

The HTTPRequestConcludingAsyncReader is a specialized reader for HTTP request bodies that enables incremental reading of body chunks followed by optional HTTP trailer fields. This type implements the ConcludingAsyncReader protocol (defined in the swift-http-api-proposal repository).

public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable {
  // ...
}

@available(*, unavailable)
extension HTTPRequestConcludingAsyncReader: Sendable {}

This reader allows you to consume the request body asynchronously, chunk by chunk, and then conclude with optional trailers arriving at the end of the request. Being non-copyable means this type uses Swift's ownership system to ensure that each request body is consumed exactly once, also preventing accidental double-reads or resource leaks.

For more details on the ConcludingAsyncReader pattern and how to use it, see the AsyncStreaming types defined in the swift-http-api-proposal repository.

HTTPResponseConcludingAsyncWriter

Mirroring the request reader, the HTTPResponseConcludingAsyncWriter is a specialised writer for HTTP response bodies that enables incremental writing of response bodies followed by optional HTTP trailer fields. This type implements the ConcludingAsyncWriter protocol (also defined in the swift-http-api-proposal repository).

public struct HTTPResponseConcludingAsyncWriter: ConcludingAsyncWriter, ~Copyable {
    // ...
}

@available(*, unavailable)
extension HTTPResponseConcludingAsyncWriter: Sendable {}

Like the request reader, this type is non-copyable to enforce single-use semantics through Swift's ownership system. Once you've written and concluded a response, the type system prevents you from accidentally writing to it again.

For more details on the ConcludingAsyncWriter pattern and how to use it, see the AsyncStreaming types defined in the swift-http-api-proposal repository.


How You Can Help

As said above, this API is still in its infancy and we want everyone to provide feedback to improve it as much as we can.

You can find the existing HTTP Server abstract APIs (and the used AsyncStreaming readers and writers) in GitHub - apple/swift-http-api-proposal: This repository contains a proposal for standardized HTTP client and server APIs for the Swift ecosystem · GitHub, and the NIO implementation at GitHub - swift-server/swift-http-server · GitHub.

Please leave feedback either as responses to this post, or as issues in the repositories. Please tag any related forum posts with http.

Note: you will need the latest 6.3 development snapshot to use the current early builds. You can download it from Install Swift | Swift.org or install it via swiftly with swiftly install 6.3-snapshot.

Links

:package: Repositories

:crystal_ball: Networking Vision

:mobile_phone: HTTPClient API

22 Likes

This is great stuff. Defining a clear API layer between server implementation and framework has been something we are already doing with Hummingbird. But to be able to do this across the whole eco-system opens a whole load of additional possibilities.

Have you considered making the HTTPRequestContext generic as well. I currently don’t see anyway to pass server implementation details to the framework layer. For instance a framework might want the remote IP address to perform rate limiting. With the current protocol design I don’t see how this could be achieved. You could just add these values to HTTPRequestContext but there might be other values, as yet undefined, that server implementations want to pass. I guess you could do this with Task locals but isn’t it preferable we test for the existence of any additional context information at compile time over run time.

I’m not a great fan of some of the symbols and find them quite a mouthful egHTTPRequestConcludingAsyncReader. But I don’t have any sensible alternatives.

The NIO server implementation is dependent on both NIOSSL, NIOHTTP2 and related libraries. Hummingbird has always tried to only include the dependencies required eg a vanilla HTTP/1.1 server wouldn’t required compiling and linking TLS, HTTP2 code. If we were to move to the NIO default implementation we would lose that. Would using traits to enable/disable elements of the server implementation be something you would consider looking into?

In whole though this is great move forward.

7 Likes

Thank you!

Yes, this may have warranted an aside in the post, actually: HTTPRequestContext is really sort of a placeholder at this point. We briefly discussed with some folks what this should be, and Generics and Task Locals were both considered, but ultimately we decided it was probably better to open this particular topic for discussion with the community, since (as you rightly pointed out) there are several ways of going about it.

I suggest we discuss this over on GitHub, since we've got this issue: Implementation-specific request contexts · Issue #28 · swift-server/swift-http-server · GitHub

I do agree names are a mouthful :slight_smile: and I'll be happy to do some bikeshedding.

I think this is a good suggestion and something we should consider. I've created Consider using traits (or some other mechanism) to remove overhead of unused features · Issue #66 · swift-server/swift-http-server · GitHub

4 Likes

First of all, thank you very much for all the work you’ve put into this!

I’ve been going through the API, and I might have missed something, so I’d like to ask for a bit of clarification. I was wondering about the possibility of sending a response body without unnecessary copies, perhaps by using some kind of intermediate type or zero-copy approach?

To illustrate with a concrete example: when sending a JSON response, the typical flow seems to be: we encode our model into Data, and then we copy that data into NIO ByteBuffer. For large responses, these extra copies can significantly impact performance, and I'd really like to find a way to avoid them.

Perhaps we could design some kind of intermediate type that can be consumed later without copying by "taking" its contents rather than copying them.

Something like that:

public struct RawBytes: ~Copyable {
   let pointer: UnsafeMutableRawPointer?
   var count: Int
}

struct CustomData: ~Copyable {
  ... 
  consuming func takeRawBytes() -> RawBytes {
    discard self
    return RawBytes(...)
  }
  ...
}

// in serve handler
try await writer.writeAndConclude(customData.takeRawBytes(), finalElement: nil)


// Init ByteBuffer from RawBytes
ByteBuffer(rawBytes: bytes)

The issue with this is Data as it currently exists isn’t a great type for this kind of thing and the HTTP Server, and by extension NIO’s ByteBuffer need to support streaming and other features that Data currently doesn’t. I imagine some part of the networking vision is to push for Swift to provide something like a writeable Span that would allow you to take the output from a JSON encoder, or other encoder and feed it directly into the response without any copies, much in the same way your example works, without the unsafe types

4 Likes

Thanks for going over the proposal. Pretty much what @0xTim has said: sadly we're currently limited by the types we've got available. We're aware of the copies here, but the plan is to get some new types in place that will make all of this more efficient.

4 Likes

Clearly I have misunderstood the “Networking Vision” document. My feeling after reading it was that the NIO thing is old and that we should be looking for something better, particularly because the Swift language has moved on considerably. Here though, you are basing the whole stack on NIO in spite of what the networking vision document says?

Yes, I'm quite familiar with the current limitations of Data. I simply wanted to raise this point because it's quite important for server-side Swift.

Quite the opposite. Quoting the original post:

We intend to build a SwiftNIO-based version of this API because SwiftNIO exists, today, and is capable of handling these use-cases, today. That allows the community to get experience with these APIs and validate that they do the necessary work.

When alternatives to or replacements for NIO arrive, they will be able to slot underneath this API. This will enable applications to be moved onto something newer without needing to completely rebuild.

But note that the Networking vision document quite explicitly disclaims your reading of it:

Is the long-term vision to phase out SwiftNIO?

No. We're unifying the underlying primitives across SwiftNIO, Network.framework, URLSession, and others to reduce duplication and increase cross-platform consistency.

NIO will remain supported for the foreseeable future.

5 Likes

Is the intention that the HTTP client aspect of this vision would be used by apps that would ordinarily use URLSession for HTTP requests?

1 Like

It reads that way to me. The vision doc speaks of URLSession being one of several implementations.

Hey Kiel, would you mind posting questions related to the Client API in the other post?

per proposal there is the intent to provide for a http server api.
yet network apis can be distinguished at a lower level aswell, having a distinction between a stream based and a state based network api, the http server api stand build upon.

essentially the postgresnio/valkey connection pool module with a subscription model for a generic server/client instance for connection pooling/registry would simplify the instantiation of clients and servers tremendously.

most favorable would be the community driven approach to common pitfalls, aswell as the discussion/inclusion/exclusion of certain exceptions/norms etc. that could improve clients/servers independent from the customization at the api surface they expose.

here is the "most recent" discussion, i could find.

the http server is a nodge in the right direction but is goes already too far excluding the possibilities inbetween?