Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Examples/ExampleMiddleware/ForwardingMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
public import Middleware

public struct ForwardingMiddleware<Input: ~Copyable & ~Escapable>: Middleware {
public init() {}

public func intercept<Return: ~Copyable>(
input: consuming Input,
next: (consuming Input) async throws -> Return
) async throws -> Return {
try await next(input)
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension Middleware {
public func forwarding() -> ForwardingMiddleware<Input> {
ForwardingMiddleware()
}
}
263 changes: 263 additions & 0 deletions Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP API Proposal open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public import HTTPAPIs
public import Logging
public import Middleware

/// A middleware that logs HTTP server requests and responses.
///
/// ``HTTPServerLoggingMiddleware`` wraps the request reader and response writer with logging
/// decorators that output information about the HTTP request path, method, response status,
/// and the number of bytes read from the request body and written to the response body.
/// This middleware is useful for debugging and monitoring HTTP traffic.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPServerLoggingMiddleware<
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable
>: Middleware
where
RequestConcludingAsyncReader: Escapable,
RequestConcludingAsyncReader.Underlying.ReadElement == UInt8,
RequestConcludingAsyncReader.FinalElement == HTTPFields?,
ResponseConcludingAsyncWriter: Escapable,
ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8,
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
{
public typealias Input = HTTPServerMiddlewareInput<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
public typealias NextInput = HTTPServerMiddlewareInput<
HTTPRequestLoggingConcludingAsyncReader<RequestConcludingAsyncReader>,
HTTPResponseLoggingConcludingAsyncWriter<ResponseConcludingAsyncWriter>
>

let logger: Logger

/// Creates a new logging middleware.
///
/// - Parameters:
/// - requestConcludingAsyncReaderType: The type of the request reader. Defaults to the inferred type.
/// - responseConcludingAsyncWriterType: The type of the response writer. Defaults to the inferred type.
/// - logger: The logger instance to use for logging HTTP events.
public init(
requestConcludingAsyncReaderType: RequestConcludingAsyncReader.Type = RequestConcludingAsyncReader.self,
responseConcludingAsyncWriterType: ResponseConcludingAsyncWriter.Type = ResponseConcludingAsyncWriter.self,
logger: Logger
) {
self.logger = logger
}

public func intercept<Return: ~Copyable>(
input: consuming Input,
next: (consuming NextInput) async throws -> Return
) async throws -> Return {
try await input.withContents { request, context, requestReader, responseSender in
self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)")
defer {
self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)")
}
let wrappedReader = HTTPRequestLoggingConcludingAsyncReader(
base: requestReader,
logger: self.logger
)

var maybeSender = Optional(responseSender)
let requestResponseBox = HTTPServerMiddlewareInput(
request: request,
requestContext: context,
requestReader: wrappedReader,
responseSender: HTTPResponseSender { [logger] response in
if let sender = maybeSender.take() {
logger.info("Sending response \(response)")
let writer = try await sender.send(response)
return HTTPResponseLoggingConcludingAsyncWriter(
base: writer,
logger: logger
)
} else {
fatalError("Called closure more than once")
}
} sendInformational: { response in
self.logger.info("Sending informational response \(response)")
try await maybeSender?.sendInformational(response)
}
)
return try await next(requestResponseBox)
}
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension Middleware {
/// Creates logging middleware for HTTP servers.
///
/// This middleware logs all incoming requests and outgoing responses, including the request
/// path, method, response status, and the number of bytes read and written in the body.
///
/// - Parameter logger: The logger to use for logging requests and responses.
/// - Returns: A middleware that logs HTTP request and response details.
///
/// ## Example
///
/// ```swift
/// @MiddlewareBuilder
/// func buildMiddleware() -> some Middleware<...> {
/// .logging(logger: Logger(label: "HTTPServer"))
/// .requestHandler()
/// }
/// ```
public func logging<RequestReader, ResponseWriter>(
logger: Logger
) -> HTTPServerLoggingMiddleware<RequestReader, ResponseWriter>
where
Input == HTTPServerMiddlewareInput<RequestReader, ResponseWriter>,
RequestReader: ConcludingAsyncReader & ~Copyable & Escapable,
RequestReader.Underlying.ReadElement == UInt8,
RequestReader.FinalElement == HTTPFields?,
ResponseWriter: ConcludingAsyncWriter & ~Copyable & Escapable,
ResponseWriter.Underlying.WriteElement == UInt8,
ResponseWriter.FinalElement == HTTPFields?
{
HTTPServerLoggingMiddleware(logger: logger)
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPRequestLoggingConcludingAsyncReader<
Base: ConcludingAsyncReader & ~Copyable
>: ConcludingAsyncReader, ~Copyable
where
Base.Underlying.ReadElement == UInt8,
Base.FinalElement == HTTPFields?
{
public typealias Underlying = RequestBodyAsyncReader
public typealias FinalElement = HTTPFields?

public struct RequestBodyAsyncReader: AsyncReader, ~Copyable, ~Escapable {
public typealias ReadElement = UInt8
public typealias ReadFailure = Base.Underlying.ReadFailure

private var underlying: Base.Underlying
private let logger: Logger

@_lifetime(copy underlying)
init(underlying: consuming Base.Underlying, logger: Logger) {
self.underlying = underlying
self.logger = logger
}

@_lifetime(self: copy self)
public mutating func read<Return, Failure>(
maximumCount: Int?,
body: (consuming Span<UInt8>) async throws(Failure) -> Return
) async throws(EitherError<Base.Underlying.ReadFailure, Failure>) -> Return {
return try await self.underlying.read(
maximumCount: maximumCount
) { (span: Span<UInt8>) async throws(Failure) -> Return in
logger.info("Received next chunk \(span.count)")
return try await body(span)
}
}
}

private var base: Base
private let logger: Logger

init(base: consuming Base, logger: Logger) {
self.base = base
self.logger = logger
}

public consuming func consumeAndConclude<Return, Failure>(
body: (consuming sending RequestBodyAsyncReader) async throws(Failure) -> Return
) async throws(Failure) -> (Return, HTTPTypes.HTTPFields?) {
let (result, trailers) = try await self.base.consumeAndConclude { [logger] reader async throws(Failure) -> Return in
let wrappedReader = RequestBodyAsyncReader(
underlying: reader,
logger: logger
)
return try await body(wrappedReader)
}

if let trailers {
self.logger.info("Received request trailers \(trailers)")
} else {
self.logger.info("Received no request trailers")
}

return (result, trailers)
}
}

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPResponseLoggingConcludingAsyncWriter<
Base: ConcludingAsyncWriter & ~Copyable
>: ConcludingAsyncWriter, ~Copyable
where
Base.Underlying.WriteElement == UInt8,
Base.FinalElement == HTTPFields?
{
public typealias Underlying = ResponseBodyAsyncWriter
public typealias FinalElement = HTTPFields?

public struct ResponseBodyAsyncWriter: AsyncWriter, ~Copyable, ~Escapable {
public typealias WriteElement = UInt8
public typealias WriteFailure = Base.Underlying.WriteFailure

private var underlying: Base.Underlying
private let logger: Logger

@_lifetime(copy underlying)
init(underlying: consuming Base.Underlying, logger: Logger) {
self.underlying = underlying
self.logger = logger
}

@_lifetime(self: copy self)
public mutating func write<Result, Failure>(
_ body: (inout OutputSpan<UInt8>) async throws(Failure) -> Result
) async throws(EitherError<Base.Underlying.WriteFailure, Failure>) -> Result {
return try await self.underlying.write { (outputSpan: inout OutputSpan<UInt8>) async throws(Failure) -> Result in
defer {
self.logger.info("Wrote response bytes \(outputSpan.count)")
}
return try await body(&outputSpan)
}
}
}

private var base: Base
private let logger: Logger

init(base: consuming Base, logger: Logger) {
self.base = base
self.logger = logger
}

public consuming func produceAndConclude<Return>(
body: (consuming sending ResponseBodyAsyncWriter) async throws -> (Return, HTTPFields?)
) async throws -> Return {
let logger = self.logger
return try await self.base.produceAndConclude { writer in
let wrappedAsyncWriter = ResponseBodyAsyncWriter(underlying: writer, logger: logger)
let (result, trailers) = try await body(wrappedAsyncWriter)

if let trailers {
logger.info("Wrote response trailers \(trailers)")
} else {
logger.info("Wrote no response trailers")
}
return (result, trailers)
}
}
}
82 changes: 82 additions & 0 deletions Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP API Proposal open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public import HTTPAPIs

/// A struct that encapsulates all parameters passed to HTTP server request handlers.
///
/// ``HTTPServerMiddlewareInput`` serves as a container for the request, request context,
/// request body reader, and response sender. This boxing is necessary because some of these
/// parameters are `~Copyable` types that cannot be stored in tuples, and it provides a
/// convenient way to pass all request-handling components through the middleware chain.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPServerMiddlewareInput<
RequestReader: ConcludingAsyncReader & ~Copyable,
ResponseWriter: ConcludingAsyncWriter & ~Copyable
>: ~Copyable {
private let request: HTTPRequest
private let requestContext: HTTPRequestContext
private let requestReader: RequestReader
private let responseSender: HTTPResponseSender<ResponseWriter>

/// Creates a new HTTP server middleware input container.
///
/// - Parameters:
/// - request: The HTTP request headers and metadata.
/// - requestContext: Additional context information for the request.
/// - requestReader: A reader for accessing the request body data and trailing headers.
/// - responseSender: A sender for transmitting the HTTP response and response body.
public init(
request: HTTPRequest,
requestContext: HTTPRequestContext,
requestReader: consuming RequestReader,
responseSender: consuming HTTPResponseSender<ResponseWriter>
) {
self.request = request
self.requestContext = requestContext
self.requestReader = requestReader
self.responseSender = responseSender
}

/// Provides scoped access to the contents of this input container.
///
/// This method exposes all the encapsulated request components to a closure, allowing
/// middleware to access and process them. The closure receives the request, request context,
/// request reader, and response sender as separate parameters.
///
/// - Parameter handler: A closure that processes the request components.
///
/// - Returns: The value returned by the handler closure.
///
/// - Throws: Any error thrown by the handler closure.
public consuming func withContents<Return: ~Copyable>(
_ handler:
(
HTTPRequest,
HTTPRequestContext,
consuming RequestReader,
consuming HTTPResponseSender<ResponseWriter>
) async throws -> Return
) async throws -> Return {
try await handler(
self.request,
self.requestContext,
self.requestReader,
self.responseSender
)
}
}

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