July 10, 2023·5 min read·engineeringswifthttpnetworkinginteroperability

Swift HTTP Types shares message metadata across networking libraries

Common request, response, and field types reduce framework conversions while leaving body handling to existing Foundation and SwiftNIO mechanisms.

HTTP code often has to translate a request before it can pass it to another library. The method and fields may describe the same message, yet the libraries expose different Swift representations. Shared policy code then depends on one framework or maintains conversions between several of them.

The July 10 Swift HTTP Types announcement proposes common types for this problem. HTTPRequest and HTTPResponse represent core message metadata for clients and servers. The package aims to make those values usable across networking libraries, with representations focused on HTTP/2 and HTTP/3 while retaining HTTP/1.1 compatibility.

I find this a useful application of Swift's type system. A request's method and fields are concepts shared by HTTP participants, so application code benefits when it can describe them without first choosing a particular library's message wrapper.

The existing split

The announcement identifies Foundation's URLSession as the system HTTP implementation exposed on Apple platforms. For server projects, it points to SwiftNIO as the recommended HTTP stack. Both ecosystems need to represent requests and responses, but their existing message types are different.

Swift HTTP Types aims eventually to replace SwiftNIO's HTTPRequestHead and HTTPResponseHead and the message details in Foundation's URLRequest and URLResponse. That is a goal as the package matures. The launch announcement describes a starting point for community feedback, not a completed migration of those frameworks.

The distinction matters when assessing what a shared type can do. Publishing it creates a common representation that integrations can adopt. Existing code does not become compatible merely because the package exists, and framework integration still needs implementation work.

The new types are not tied to either existing framework. That permits a library to express HTTP metadata without using a Foundation-specific request as its public contract or requiring a NIO channel context simply to inspect a method and path. For shared client and server logic, the announcement identifies removal of conversions between framework-specific message types as a direct benefit of adoption across projects.

A request exposes HTTP components

The launch example constructs a request through its method, scheme, authority, and path. This is a direct description of message components, without requiring a Foundation URL to be the initial representation.

import HTTPTypes

var request = HTTPRequest(
    method: .get,
    scheme: "https",
    authority: "www.example.com",
    path: "/"
)

request.method = .post
request.path = "/upload"
request.headerFields[.userAgent] = "ExampleApp/1.0"

let response = HTTPResponse(status: .ok)

The announcement also demonstrates constructing a request from a Foundation URL. That convenience is useful at a framework boundary, but the component-based representation remains available when a caller already has HTTP metadata.

Mutation of the method or path is explicit in the example. A shared representation allows code operating on that metadata to use the same properties wherever a compatible integration supplies the message. The benefit is reducing repeated conversion code, not changing the semantics of the HTTP operation itself.

The response example is correspondingly small because creating an HTTPResponse with an OK status only describes the response metadata. It does not create a server, send bytes, or select a body representation. Those responsibilities remain elsewhere.

Header fields have both names and multiplicity

The field API includes common names such as userAgent and acceptLanguage. Callers can extend HTTPField.Name with a custom name and use the same subscript syntax for it. That gives custom fields a place in the naming model without requiring a separate mechanism for application-specific metadata.

Multiple values are also visible. The launch example assigns an array through the raw field subscript and retrieves either the individual values or a combined representation.

request.headerFields[raw: .acceptLanguage] = [
    "en-US",
    "zh-Hans-CN"
]

let values = request.headerFields[raw: .acceptLanguage]
let combined = request.headerFields[.acceptLanguage]

For this example, the ordinary accessor produces the combined language value, while the raw accessor retains the separate strings. The example makes the distinction inspectable in code. A generic map from field name to one arbitrary string would conceal that choice.

Typed field names also support editor completion, which the announcement demonstrates when setting User-Agent. This is a modest but useful gain for repeated HTTP work. The name is part of the API instead of being a string literal copied independently at every call site.

The package leaves bodies outside its initial scope

Request and response bodies are explicitly absent from the package's initial scope. Foundation callers continue to use Data and InputStream. SwiftNIO callers continue to use ByteBuffer. The project invites discussion about body handling in the future.

That boundary limits how far a shared HTTPRequest can unify an application's networking code today. A component that only examines request metadata can use the common types. A component that consumes or produces bodies still needs the relevant framework's mechanisms.

This separation also makes the announcement easier to evaluate. The project can establish agreement on methods, status codes, and fields without claiming to have resolved every issue involved in body transport. A common message type is useful even while the body remains framework-specific.

The launch post demonstrates URLSession integration and describes planned SwiftNIO integration through swift-nio-extras once the package becomes stable. Its NIO example places a codec before the application channel handler. The handler receives request metadata, body parts, and an end marker through the integration's request-part type.

A different layer from OpenAPI generation

June's Swift OpenAPI Generator introduction addressed a related problem at the operation level. It generates typed calls and server interfaces from a service description, then connects them to HTTP libraries through transport protocols.

HTTP Types works at a lower level of description. It represents an HTTP message's common metadata without requiring the message to belong to a documented application operation. An API generator needs a service contract, while a generic HTTP intermediary may need to process requests independent of any such contract.

I prefer a clear boundary between those jobs. Generating a greeting operation and representing a request's fields require different information. Shared HTTP metadata can support library interoperability without assuming that every caller possesses an OpenAPI document.

At launch, the useful outcome is a concrete common vocabulary that integrations can adopt. A request method, a response status, and a field collection can move into shared code as libraries support the package. Body handling and the planned replacement of existing framework types remain separate work described by the announcement.