|

swift-http-types: Giving Swift a Shared Vocabulary for HTTP

Daily (almost) Swift Open Source Overview hero for swift-http-types

HTTP is everywhere in Swift, but the type representing an HTTP request often depends on where the code runs. A client may use URLRequest, a server framework may have its own request type, and a networking library may define another one.

That becomes awkward when code needs to move between those layers. Middleware, tracing, authentication, testing, proxies, and shared libraries often care about HTTP semantics, not about the transport that happened to carry the message.

swift-http-types takes a deliberately smaller role: it defines common HTTP message and field types without trying to become an HTTP client or server.

The useful split: HTTP message vs transport

The core package exposes HTTPRequest, HTTPResponse, HTTPField, and HTTPFields. Package.swift currently gives the HTTPTypes target no external package dependencies.

That design matters. A function can accept an HTTPRequest without also choosing URLSession, SwiftNIO, Vapor, or another networking stack. Transport adapters can convert their own representation at the edge while shared code works with the same HTTP vocabulary.

There is a second product, HTTPTypesFoundation, for code that does want Foundation integration. It bridges the core types to URLRequest, HTTPURLResponse, and URLSession instead of putting Foundation-specific behavior into the core message model.

Example 1: use the same request type with URLSession

Here is a small client example written against the current API:

import Foundation
import HTTPTypes
import HTTPTypesFoundation

var request = HTTPRequest(
    method: .get,
    url: URL(string: "https://api.example.com/books")!
)
request.headerFields[.accept] = "application/json"
request.headerFields[.authorization] = "Bearer example-token"

let (data, response) = try await URLSession.shared.data(for: request)

print(response.status)
print(data.count)

The interesting part is not that this replaces URLSession. It does not. HTTPTypesFoundation converts the HTTPRequest into a URLRequest, asks URLSession to perform the request, then converts the resulting HTTPURLResponse back into an HTTPResponse.

So the transport is still Foundation, but the message type can be shared with code that does not otherwise need Foundation networking types.

Requests model modern HTTP concepts

HTTPRequest stores the request method separately and exposes conveniences for the pseudo-header concepts used by modern HTTP: scheme, authority, path, and extended CONNECT protocol. The source explicitly describes how these map into a legacy HTTP/1 context, where scheme is ignored and authority becomes the Host header.

This is a useful detail because it keeps the model from being only an HTTP/1 header dictionary with a method attached. The same request representation can describe concepts used by newer HTTP versions without forcing application code to manually manipulate strings such as :authority.

The request and response types are both Sendable and Hashable, which also makes them practical currency types for concurrent Swift code.

Header fields are more than a dictionary

One of the more interesting parts of the package is HTTPFields.

HTTP headers look dictionary-like until repeated fields and protocol rules matter. HTTPFields is an ordered collection of HTTPField values, not simply [String: String]. Multiple fields with the same name can exist and their order is preserved.

The convenience subscript still gives dictionary-like access:

var fields = HTTPFields()
fields[.accept] = "application/json"
fields[.cacheControl] = "no-cache"

But its behavior follows HTTP-specific rules. When several fields have the same name, reading through the name subscript joins their values with commas. Cookie is treated differently and uses a semicolon separator. Setting a value by name replaces the fields with that name; assigning nil removes them.

For code that needs repeated values directly, the API also exposes subscripts returning arrays of values or complete HTTPField objects.

Field names preserve spelling but compare without case

HTTPField.Name is another small abstraction that removes a surprising amount of repeated protocol code.

HTTP field names are case-insensitive. The type keeps the original spelling while also maintaining a canonical representation for comparison and lookup. Initializers validate allowed characters according to the token rules referenced from RFC 9110.

That means application code can work with .contentType, .authorization, or a validated custom name rather than repeatedly lowercasing arbitrary strings and hoping every layer applies the same rules.

HTTPField also keeps the raw field value representation behind a string convenience API. Its public initializer legalizes invalid field-value bytes instead of allowing arbitrary invalid values through the normal path, while withUnsafeBytesOfValue exists for code that needs access to the underlying bytes.

A detail aimed at HTTP/2 and HTTP/3 implementations

Each HTTPField carries a DynamicTableIndexingStrategy with .automatic, .prefer, .avoid, and .disallow options.

This is not something most application code will touch. It exists for the HPACK and QPACK dynamic tables used by HTTP/2 and HTTP/3. The important design point is that metadata needed by a protocol implementation can travel with the field without changing its name/value API for ordinary callers.

The package also now contains HTTP2ErrorCode and HTTP3ErrorCode; those types were added in the 1.7.0 release.

Example 2: shared HTTP logic without choosing a server framework

A shared component can operate on the common message types directly:

import HTTPTypes

func requireJSON(_ request: HTTPRequest) -> HTTPResponse? {
    guard request.headerFields[.contentType] == "application/json" else {
        var response = HTTPResponse(status: .unsupportedMediaType)
        response.headerFields[.contentType] = "text/plain"
        return response
    }

    return nil
}

There is no server loop here and no framework-specific request object. A server adapter could call this function after converting its incoming request to HTTPRequest and translate the returned HTTPResponse back to its transport representation.

That is where I find the package most useful conceptually: it gives libraries a small HTTP-facing API without making them depend on the application’s networking framework.

What the implementation is doing

The implementation is compact, but it is not just a collection of type aliases.

HTTPFields is currently backed by a simple [HTTPField]. Lookup scans by the field name’s canonical form. Setting fields of a particular name performs a single pass over existing matching entries, overwrites where possible, removes leftovers, and appends additional values when needed.

The equality implementation is more involved because HTTP field order has unusual semantics: values with the same name must preserve their relative order, while fields with different names may be interleaved differently and still compare equal. The code starts with a lock-step comparison and can switch to an indexed strategy for larger amounts of disorder.

HTTPRequest.PseudoHeaderFields takes a different approach. Its storage is a reference type behind a value-type API and uses isKnownUniquelyReferenced before mutation, giving the struct copy-on-write behavior for its pseudo-header storage.

These are implementation choices worth reading because they show the package balancing HTTP semantics with the cost of a type that may sit on a hot networking path.

What it deliberately does not do

swift-http-types does not send requests, listen on sockets, route endpoints, or provide a complete structured model for every HTTP header.

For example, the current HTTPFields API has special storage behavior for Cookie, but open issue #142 proposes adding APIs for parsing and serializing cookies. So it would be inaccurate to describe the package as a complete cookie API today.

There is also an open PR, #152, describing an equality/hash consistency problem on a particular HTTPFields equality path when fields differ only in metadata or raw representation. The proposed change includes a regression test. Until that is resolved upstream, code that relies on those edge cases should treat it as an active issue rather than assume the behavior is settled.

Who should look at it

The package is especially relevant when HTTP-shaped data needs to cross library or framework layers: reusable middleware, client/server shared code, observability libraries, protocol adapters, tests, or infrastructure that should not adopt a large networking dependency just to describe a request.

If an application only makes a few Foundation requests and never exposes HTTP messages outside that code, URLRequest and HTTPURLResponse may already be enough. Adding another representation without a place where the shared type matters would only add conversion work.

What is worth reading in the source

Start with HTTPRequest.swift, HTTPResponse.swift, and HTTPFields.swift. They show most of the public model and the less obvious field semantics.

Then read HTTPFieldName.swift and HTTPFieldValue.swift if you want to see where protocol validation and canonicalization happen. URLRequest+HTTPTypes.swift, URLResponse+HTTPTypes.swift, and URLSession+HTTPTypes.swift show how the Foundation integration stays outside the core target.

The tests are useful here because many of the important behaviors are edge cases: field-name casing, repeated fields, value legalization, URL conversion, equality, and hashing.

Project health

The repository is public, active, and Apache-2.0 licensed. The latest GitHub release I verified is 1.8.0, published on September 7, 2026. That release added an HTTP/3 datagram error code and included fixes around parsed field-name validation.

The 1.7.0 release just before it contained a notable group of performance changes: HTTPFields was reimplemented on a simple array, equality and hashing work changed, benchmarks were added, and several field/request operations were optimized.

Swift Package Index currently reports successful build information across Apple platforms, Linux, Wasm, and Android. I would treat that as package-index compatibility evidence, not as a platform list declared by Package.swift, because the manifest itself does not declare platforms.

The useful idea

The package is a good example of a small interoperability layer. It does not try to own networking. It gives networking code a shared representation of HTTP.

That distinction is what makes it useful: HTTPRequest and HTTPResponse can sit between transports, frameworks, and libraries, while HTTPField and HTTPFields keep enough protocol semantics that the shared representation is more precise than a bag of strings.

Sources