import Elementary import Foundation import Hummingbird /// 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. The page is served in the language stored on the context by /// ``LocalizationMiddleware``, falling back to the default language. /// /// Its responses declare `Vary: Accept-Language`: where the page routes pin their language by URL, this is the one responder that negotiates. /// An unrouted path names no edition, so no canonical URL contradicts the header. public struct NotFoundMiddleware { // MARK: Properties /// 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( bundle: Bundle, document: (Locale) -> Document ) { 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: (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: context.language, request: request ) } } }