SOAR-0005: Adopting the Swift HTTP Types package
Adopt the new ecosystem-wide Swift HTTP Types package for HTTP currency types in the transport and middleware protocols.
Overview
Proposal: SOAR-0005
Author(s): Honza Dvorsky
Status: Implemented (0.3.0)
Implementation: apple/swift-openapi-generator#245, apple/swift-openapi-runtime#47, apple/swift-openapi-urlsession#15, swift-server/swift-openapi-async-http-client#16
Review: (review)
Affected components: generator, runtime, client transports, server transports
Versions:
v1 (2023-09-08): Initial version
v1.1: In
ServerTransport
andServerMiddleware
, make the responseHTTPBody
optional to preserve the intent of the OpenAPI document author for the transport.v1.2 (2023-09-13): Made response bodies optional.
Introduction
Use the HTTP request and response types from the Swift HTTP Types package instead of maintaining our own currency types.
Motivation
The purpose of the Swift OpenAPI Runtime library is to be an “API package”, in the meaning established by earlier packages in the Swift ecosystem, such as swift-log. The API package contains generally useful abstractions, which adopters can implement their logic against, and plug in any concrete implementation provided by anyone else in the ecosystem.
Similarly in Swift OpenAPI Generator, the most important abstractions in the runtime library are the transport and middleware protocols. There are concrete implementations of client and server protocols maintained as separate packages, and an adopter can use any of those concrete implementations with the adopter’s generated code. No need to regenerate any code when switching between different transport implementations.
// Instantiate a concrete transport implementation.
let transport: any ClientTransport = ... // any client transport
// Instantiate the generated client by providing it with the transport.
let client = Client(transport: transport)
// Make API calls
let response = try await client.getStats(...)
...
The definitions of the transport and middleware protocols currently use currency types provided by the runtime library, representing the HTTP request and response.
These types need to be maintained and them being specific to the runtime library makes implementing a transport or middleware a little more work, as you need to write code that converts, for example, from OpenAPIRuntime.Request
to Foundation.URLRequest
.
A new package open sourced earlier in 2023 called Swift HTTP Types opens the door to unifying the HTTP request and response types across the Swift ecosystem. By adopting it, we can get following benefits:
No need to maintain
OpenAPIRuntime.Request
,OpenAPIRuntime.Response
, and friends.Get improvements to Swift HTTP Types for free.
Simplify implementing concrete transports and middlewares for libraries that also use Swift HTTP Types (as no manual conversion needed).
Proposed solution
We propose to use the HTTPRequest
and HTTPResponse
types (and other types they refer to, such as HTTPFields
) provided by Swift HTTP Types in the transport and middleware protocols, and delete the request, response, and related types from the runtime library.
Detailed design
Let’s compare each of the transport and middleware protocols as they are today to what they can be with Swift HTTP Types. Note that in the code snippets below, details not relevant to the proposal, such as code comments, are omitted.
Client transport
The existing ClientTransport
protocol:
public protocol ClientTransport {
func send(
_ request: Request,
baseURL: URL,
operationID: String
) async throws -> Response
}
The proposed ClientTransport
protocol:
public protocol ClientTransport {
func send(
_ request: HTTPRequest, // <<< changed
body: HTTPBody?, // <<< added
baseURL: URL,
operationID: String
) async throws -> (HTTPResponse, HTTPBody?) // <<< changed
}
Notable changes:
The request and response bodies are passed separately from the request and response metadata types.
The fully qualified type names used are:
HTTPTypes.HTTPRequest
HTTPTypes.HTTPResponse
OpenAPIRuntime.HTTPBody
Client middleware
The existing ClientMiddleware
protocol:
public protocol ClientMiddleware: Sendable {
func intercept(
_ request: Request,
baseURL: URL,
operationID: String,
next: @Sendable (Request, URL) async throws -> Response
) async throws -> Response
}
The proposed ClientMiddleware
protocol:
public protocol ClientMiddleware {
func intercept(
_ request: HTTPRequest, // <<< changed
body: HTTPBody?, // <<< added
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) // <<< changed
) async throws -> (HTTPResponse, HTTPBody?) // <<< changed
}
The changes are of the same nature as those to the client transport.
Server transport
The existing ServerTransport
protocol:
public protocol ServerTransport {
func register(
_ handler: @Sendable @escaping (Request, ServerRequestMetadata) async throws -> Response,
method: HTTPMethod,
path: [RouterPathComponent],
queryItemNames: Set<String>
) throws
}
The proposed ServerTransport
protocol:
public protocol ServerTransport {
func register(
_ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?), // <<< changed
method: HTTPRequest.Method, // <<< changed
path: String // <<< changed
) throws
}
Notable changes:
The
OpenAPIRuntime.ServerRequestMetadata
type removes itsqueryParameters
property, as it’s not needed as of version 0.2.0, where the query string is decoded directly, instead of receiving the key-value pairs from the transport. We held off the breaking change until 0.3.0, as 0.2.0 did not make breaking changes to the transport and middleware protocols.The
path
parameter is now a string instead of an array of parsed components. The string is the templated URI, for example/pets/{petId}
. This change was made to support #199 path components that are parameters but also have a constant component, and it is up to the server transport implementation to parse the string and pull parameter names from it.Just like client transport, the request and response bodies are passed separately from the request and response metadata types.
The fully qualified type names used are:
HTTPTypes.HTTPRequest
HTTPTypes.HTTPResponse
OpenAPIRuntime.HTTPBody
Server middleware
The existing ServerMiddleware
protocol:
public protocol ServerMiddleware: Sendable {
func intercept(
_ request: Request,
metadata: ServerRequestMetadata,
operationID: String,
next: @Sendable (Request, ServerRequestMetadata) async throws -> Response
) async throws -> Response
}
The proposed ServerMiddleware
protocol:
public protocol ServerMiddleware {
func intercept(
_ request: HTTPRequest, // <<< changed
body: HTTPBody?, // <<< added
metadata: ServerRequestMetadata,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) // <<< changed
) async throws -> (HTTPResponse, HTTPBody?) // <<< changed
}
The changes are of the same nature as those to client transport, client middleware, and server transport.
API stability
The users of concrete transport and middleware implementations will not have to make any changes, only the implementors of transports and middlewares will need to adapt their packages to work with the new currency types.
Future directions
Nothing beyond the same point about the async writer pattern from SOAR-0004.
Alternatives considered
Not adopting the package
An alternative here is to do nothing and not adopt the new package, however since the common HTTP types work so similarly to the types we had to maintain, it was never seriously considered not to take advantage of the common types. While the package is new right now, we expect wider adoption throughout the ecosystem in the coming years, at which point having our own currency types for HTTP requests and responses might be the exception rather than the rule.