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 { // 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( 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: (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 ) } } }