Files
ccn/Packages/Infrastructure/Sources/Public/Middlewares/CompressionMiddleware.swift
T

71 lines
2.4 KiB
Swift
Raw Normal View History

2026-09-20 12:45:58 +02:00
import HTTPTypes
import Hummingbird
import HummingbirdCompression
import Logging
/// Compresses the responses that are not already encoded, and passes the ones that are through untouched.
///
/// Hummingbird's `ResponseCompressionMiddleware` appends to `Content-Encoding` without checking for one, so a pre-compressed page (see
/// ``CachedHTMLResponse``) would go out as `gzip, gzip` — a client decodes once and renders the inner gzip stream. This stands in for that
/// middleware and delegates to it, so the threshold, the negotiation and the compressor stay its behaviour.
///
/// - Note: `Context` is the request context the middleware is resolved against.
public struct CompressionMiddleware<Context: RequestContext>: Sendable {
// MARK: Properties
/// The middleware the unencoded responses are handed to.
private let compression: ResponseCompressionMiddleware<Context>
// MARK: Initializers
/// Creates a compression middleware.
/// - Parameter minimumResponseSizeToCompress: the smallest response body, in bytes, that is compressed at all.
public init(
minimumResponseSizeToCompress: Int
) {
self.compression = .init(
minimumResponseSizeToCompress: minimumResponseSizeToCompress
)
}
}
// MARK: - RouterMiddleware
extension CompressionMiddleware: RouterMiddleware {
// MARK: Functions
/// Passes the request down the chain and compresses the response on the way back up, unless it already names an encoding.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - next: the next responder in the middleware chain.
/// - Returns: the downstream response, compressed when it was not already.
/// - Throws: any error thrown downstream.
public func handle(
_ request: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
let response = try await next(
request,
context
)
guard response.headers[.contentEncoding] == nil else {
return response
}
// The response is already in hand, so the delegate gets it rather than the chain — `next` runs once.
return try await compression.handle(
request,
context: context
) { _, _ in
response
}
}
}