Files
ccn/Packages/Infrastructure/Sources/Public/Middlewares/NotFoundMiddleware.swift
T
javierandClaude Opus 5.5 911424de24 Project updates from Template
This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing:

- Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305)
- Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df)
- Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9)
- Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769)
- Enabled the Analytics trait in the Website service. (f6a6bbd)
- Reworked page routing in the Website service. (d1cb2c1)
- Gated the structured data features in the Infrastructure package behind a trait. (f0a8791)
- Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3)
- Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b)

The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead.

The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:55:43 +01:00

84 lines
2.9 KiB
Swift

import Elementary
import Foundation
import Hummingbird
import Localization
/// Serves a custom error page for requests that match neither a route nor a static file.
///
/// Placed ahead of `FileMiddleware` in the middleware chain, it catches the `.notFound` error that bubbles up when no file exists for the requested
/// path and responds with the rendered error page and a `404 Not Found` status.
///
/// It negotiates the language itself, on the way out, so the cost falls on the 404s rather than on every request through the chain — where page
/// routes pin their language by URL, this is the one responder that has to ask. Its responses declare `Vary: Accept-Language` accordingly: an
/// unrouted path names no edition, so no canonical URL contradicts the header.
public struct NotFoundMiddleware<Context: RequestContext> {
// MARK: Properties
/// Negotiates the language a not-found response is served in.
private let negotiate: Negotiate
/// The error page, rendered once per supported language and reused for every not-found response.
private let responses: LocalizedHTMLCollectionResponse
// MARK: Initializers
/// Creates a not-found middleware.
/// - Parameters:
/// - bundle: the bundle whose String Catalog names the languages the page is rendered for.
/// - document: builds the error page to render for a given locale.
public init<Document: HTMLDocument>(
bundle: Bundle,
document: (Locale) -> Document
) {
self.negotiate = .init(bundle: bundle)
self.responses = .init(
bundle: bundle,
status: .notFound,
variesOnAcceptLanguage: true,
document: document
)
}
}
// MARK: - RouterMiddleware
extension NotFoundMiddleware: RouterMiddleware {
// MARK: Functions
/// Passes the request down the chain, rendering the error page if it results in a not-found response.
///
/// Any error other than `.notFound` is rethrown unchanged.
/// - 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, or the rendered error page with a `404 Not Found` status.
/// - Throws: any non-not-found error thrown downstream.
public func handle(
_ request: Request,
context: Context,
next: @concurrent (Request, Context) async throws -> Response
) async throws -> Response {
do {
return try await next(request, context)
}
catch let error {
guard
let responseError = error as? any HTTPResponseError,
responseError.status == .notFound
else {
throw error
}
return responses.response(
for: negotiate(for: request),
request: request
)
}
}
}