import Elementary import HTTPTypes import Hummingbird import NIOCore /// A pre-rendered HTTP response for a fully static HTML page. /// /// The document is rendered to bytes once, at initialization, and every ``response(for:)`` reuses those bytes — along with a fixed status and /// precomputed headers — instead of re-rendering. This suits pages whose markup never changes between requests, such as the landing page and the /// not-found page, avoiding a per-request Elementary render on hot paths. /// /// A successful page also revalidates cheaply: its headers carry a weak entity tag derived from the rendered bytes and a `Cache-Control` that asks /// clients to revalidate (`no-cache`), so a repeat visit costs a `304 Not Modified` instead of a full transfer — and a deploy that changes the page /// changes the tag, propagating immediately. /// /// ``LocalizedHTMLCollectionResponse`` builds on this type, caching one instance per supported language. /// /// The body is written as an unsized stream (no `Content-Length`), mirroring `HTMLResponse`, so the response-compression middleware downstream /// treats it exactly as it would a freshly rendered page. public struct CachedHTMLResponse: Sendable { // MARK: Properties /// The page rendered to bytes once. private let buffer: ByteBuffer /// The weak entity tag of the rendered bytes, present on successful pages only. private let eTag: String? /// The headers applied to every response, precomputed once. private let headers: HTTPFields /// The status applied to every response. private let status: HTTPResponse.Status // MARK: Initializers /// Renders the given document to bytes once. /// /// A `200 OK` page gets the revalidation headers (`ETag` and `Cache-Control`); an error page does not, since a `304 Not Modified` only /// ever stands in for a success. /// - Parameters: /// - status: the status applied to every response. Defaults to `.ok`. /// - additionalHeaders: extra headers merged onto every response, alongside the content type. /// Used to carry per-language signals such as `Content-Language` and `Vary`. /// - document: the static HTML document to render and cache. public init( status: HTTPResponse.Status = .ok, additionalHeaders: HTTPFields = [:], document: some HTMLDocument ) { let buffer = ByteBuffer(string: document.render()) var headers: HTTPFields = [ .contentType: "text/html; charset=utf-8" ] var eTag: String? if status == .ok { var hash = FNV1aHash() hash.combine(buffer.readableBytesView) eTag = "W/\"\(hash.digest)\"" headers[.eTag] = eTag headers[.cacheControl] = "public, no-cache" } for field in additionalHeaders { headers[field.name] = field.value } self.buffer = buffer self.eTag = eTag self.headers = headers self.status = status } // MARK: Methods /// Builds a response from the cached, pre-rendered bytes. /// /// A conditional request whose `If-None-Match` names the page's entity tag is answered with a /// bodyless `304 Not Modified`. Otherwise the full page is served, mirroring the /// `text/html; charset=utf-8` content type `HTMLResponse` produces and leaving the /// `Content-Length` unset so small pages remain eligible for compression. /// - Parameter request: the request the response answers. /// - Returns: the response carrying the cached HTML body, or its `304` revalidation. public func response( for request: Request ) -> Response { if let eTag, request.method == .get || request.method == .head, let match = request.headers[.ifNoneMatch], match == "*" || match.contains(eTag) { return Response( status: .notModified, headers: headers ) } return Response( status: status, headers: headers, body: .init { [buffer] writer in try await writer.write(buffer) try await writer.finish(nil) } ) } }