IBM-Swift/Kitura

View on GitHub
Sources/Kitura/Router.swift

Summary

Maintainability
B
6 hrs
Test Coverage
/*
 * Copyright IBM Corporation 2015
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import KituraNet
import LoggerAPI
import Foundation
import KituraTemplateEngine
import KituraContracts

// MARK Router

/// `Router` provides the external interface for routing requests to
/// the appropriate code to handle them. This includes:
///
///   - Routing requests to closures of type `RouterHandler`
///   - Routing requests to the handle function of classes that implement the
///    `RouterMiddleware` protocol.
///   - Routing requests to a `TemplateEngine` to generate the appropriate output.
///   - Serving the landing page when someone makes an HTTP request with a path of slash (/).
///

public class Router {
    
    /// Contains the list of routing elements
    var elements: [RouterElement] = []

    /// Map from file extensions to Template Engines
    private var templateEngines = [String: TemplateEngine]()

    /// Default template engine extension
    private var defaultEngineFileExtension: String?

    /// Prefix for special page resources
    fileprivate let kituraResourcePrefix = "/@@Kitura-router@@/"

    /// Helper for serving file resources
    fileprivate let fileResourceServer: FileResourceServer?

    /// Flag to enable/disable access to parent router's params
    private let mergeParameters: Bool

    /// Collection of `RouterParameterHandler` for specified parameter name
    /// that will be passed to `RouterElementWalker` when server receives client request
    /// and used to handle request's url parameters.
    fileprivate var parameterHandlers = [String : [RouterParameterHandler]]()

    // MARK: Initializer
    
    /// Initialize a `Router` instance.
    /// ### Usage Example: ###
    /// ```swift
    ///  let router = Router()
    /// ```
    /// #### Using `mergeParameters`: ####
    /// When initialising a `Router`, `mergeParameters` allows you to control whether
    /// the router will be able to access parameters matched in its parent router. For instance, in the example below
    /// if `mergeParameters` is set to `true`, `GET /Hello/Alex` will return "Hello Alex", but if set to `false`
    /// the `greeting` parameter will not be accessible and it will return just " Alex".
    /// ```swift
    /// let router = Router()
    /// let userRouter = Router(mergeParameters: true)
    ///
    /// router.get("/:greeting") { request, response, _ in
    ///   let greeting = request.parameters["greeting"] ?? ""
    ///   try response.send("\(greeting)").end()
    /// }
    ///
    /// userRouter.get("/:user") { request, response, _ in
    ///   let user = request.parameters["user"] ?? ""
    ///   let greeting = request.parameters["greeting"] ?? ""
    ///   try response.send("\(greeting) \(user)").end()
    /// }
    ///
    /// router.all("/:greeting", middleware: userRouter)
    /// ```
    /// - Parameter mergeParameters: Optional parameter to specify if the router should be able to access parameters
    ///                                 from its parent router. Defaults to `false` if not specified.
    /// - Parameter enableWelcomePage: Optional parameter to start and use the
    ///                                FileResourceServer to serve the default
    ///                                "Welcome to Kitura" page and related
    ///                                assets.
    /// - Parameter apiDocument: Optional parameter that allows customization of the OpenAPI document 
    ///                          describing this Router.
    public init(mergeParameters: Bool = false, enableWelcomePage: Bool = true, apiDocument: SwaggerDocument = SwaggerDocument()) {
        self.swagger = apiDocument
        self.mergeParameters = mergeParameters
        self.fileResourceServer = enableWelcomePage ? FileResourceServer() : nil

        Log.verbose("Router initialized")
    }

    func routingHelper(_ method: RouterMethod, pattern: String?, handler: [RouterHandler]) -> Router {
        elements.append(RouterElement(method: method,
                                      pattern: pattern,
                                      handler: handler,
                                      mergeParameters: mergeParameters))
        return self
    }

    func routingHelper(_ method: RouterMethod, pattern: String?, allowPartialMatch: Bool = true, middleware: [RouterMiddleware]) -> Router {
        elements.append(RouterElement(method: method,
                                      pattern: pattern,
                                      middleware: middleware,
                                      allowPartialMatch: allowPartialMatch,
                                      mergeParameters: mergeParameters))
        return self
    }

    // MARK: Swagger
    
    /// Contains the structures needed for swagger document generation
    var swagger: SwaggerDocument
    
    /// Returns the current in-memory representation of Codable routes as a
    /// Swagger document in JSON format, or nil if the document cannot be
    /// generated.
    public var swaggerJSON: String? {
        do {
            return try self.swagger.serializeAPI(format: .json)
        } catch {
            return nil
        }
    }
    
    // MARK: Custom encoder/decoder
    
    /**
     A dictionary of `MediaType` to `BodyEncoder` generators.
     By default this includes an entry mapping the "application/json" media type to a JSONEncoder generator.
     When a Codable object is sent as a response, an encoder will be generated based on the "Accepts" header
     or using the `defaultResponseMediaType` if no matching encoder is found.
     ### Usage Example: ###
     The example below replaces the default JSON encoder with a new encoder that has a different date encoding strategy.
     ```swift
     let router = Router()
     let newJSONEncoder: () -> BodyEncoder = {
         let encoder = JSONEncoder()
         encoder.dateEncodingStrategy = .secondsSince1970
         return encoder
     }
     router.encoders[.json] = newJSONEncoder
     ```
     ### Considerations for use with OpenAPI generation ###
     In order for your OpenAPI (swagger) document to correctly represent an alternate `Date` encoding that you have chosen, you _must_ configure your encoders before registering routes.
     */
    public var encoders: [MediaType: () -> BodyEncoder] = [.json: {return JSONEncoder()}] {
        didSet {
            swagger.setEncoders(self.encoders)
        }
    }
    /**
     if the request's Accept header does not match an encoder,
     this media type will be used by the `RouterResponse` to select a `BodyEncoder`.
     By default, this is set to "application/json".
     ### Usage Example: ###
     The example below sets the `defaultResponseMediaType` as "application/x-yaml".
     ```swift
     let router = Router()
     router.defaultResponseMediaType = MediaType(type: .application, subtype: "x-yaml")
     ```
     */
    public var defaultResponseMediaType: MediaType = .json
    
    /**
     A dictionary of `MediaType` to `BodyDecoder` generators.
     By default this includes an entry mapping the "application/json" media type to a JSONDecoder generator and an entry mapping "application/x-www-form-urlencoded" media type to `QueryDecoder`
     When a Codable object is read from the body of a request, a decoder will be generated based on the "Content-Type" header.
     ### Usage Example: ###
     The example below replaces the default JSON decoder with a new decoder that has a different date encoding strategy.
     ```swift
     let router = Router()
     let newJSONDecoder: () -> BodyDecoder = {
         let decoder = JSONDecoder()
         decoder.dateDecodingStrategy = .secondsSince1970
         return decoder
     }
     router.decoders[.json] = newJSONDecoder
     ```
     */
    public var decoders: [MediaType: () -> BodyDecoder] = [.json: {return JSONDecoder()}, .urlEncoded: {return QueryDecoder()}] {
        didSet {
            swagger.setDecoders(self.decoders)
        }
    }

    // MARK: Template Engine

    /// The root directory where template files should be placed in order to be automatically handed
    /// over to an appropriate templating engine for content generation. The directory should sit at the
    /// same level as the project's "Sources" directory. Defaults to "./Views/".
    /// ### Usage Example: ###
    /// The example below changes the directory where template files should be placed to be "./myViews/"
    /// ```swift
    /// let router = Router()
    /// router.viewsPath = "./myViews/"
    /// ```
    public var viewsPath = "./Views/" {
        didSet {
            for (_, templateEngine) in templateEngines {
                setRootPaths(forTemplateEngine: templateEngine)
            }
        }
    }
    
    /// Register a template engine to a given router instance.
    /// A template engine allows rendering of documents using static templates.
    ///
    /// By default the templating engine will handle files in the `./Views` directory
    /// that match the file extension it supports. You can change this default location using the `viewsPath` property.
    /// ### Usage Example: ###
    /// ```swift
    /// let router = Router()
    /// router.add(templateEngine: MyTemplateEngine())
    /// router.add(templateEngine: MyOtherTemplateEngine(), forFileExtensions: ["html"], useDefaultFileExtension: false)
    /// ```
    /// If multiple different template engines are registered for the same extension, the template engine
    /// that is registered last will be the one that attempts to render all template files with the chosen extension.
    /// - Parameter templateEngine: The templating engine to register.
    /// - Parameter forFileExtensions: The extensions of the files to apply the template engine on.
    /// - Parameter useDefaultFileExtension: The flag to specify if the default file extension of the
    ///   template engine should be used. Defaults to `true` if not specified.
    public func add(templateEngine: TemplateEngine, forFileExtensions fileExtensions: [String] = [],
                    useDefaultFileExtension: Bool = true) {
        if useDefaultFileExtension {
            templateEngines[templateEngine.fileExtension] = templateEngine
        }
        for fileExtension in fileExtensions {
            templateEngines[fileExtension] = templateEngine
        }
        setRootPaths(forTemplateEngine: templateEngine)
    }
    
    /// Sets the default templating engine to be used when the extension of a file in the
    /// `viewsPath` doesn't match the extension of one of the registered templating engines.
    /// ### Usage Example: ###
    /// ```swift
    /// let router = Router()
    /// router.setDefault(templateEngine: MyTemplateEngine())
    /// ```
    /// If the template engine doesn't provide a default extension you can provide one using
    /// `add(templateEngine:forFileExtensions:useDefaultFileExtension:)`. If a router instance doesn't
    /// have a template engine registered that can render the given template file a
    /// "No template engine defined for extension" `TemplatingError` is thrown.
    /// - Parameter templateEngine: The templating engine to set as default.
    public func setDefault(templateEngine: TemplateEngine?) {
        if let templateEngine = templateEngine {
            defaultEngineFileExtension = templateEngine.fileExtension
            add(templateEngine: templateEngine)
            return
        }
        defaultEngineFileExtension = nil
    }

    private func setRootPaths(forTemplateEngine templateEngine: TemplateEngine) {
        let absoluteViewsPath = StaticFileServer.ResourcePathHandler.getAbsolutePath(for: viewsPath)
        templateEngine.setRootPaths(rootPaths: [absoluteViewsPath])
    }

    /// Render a template using a context
    /// - Parameter template: The path to the template file to be rendered.
    /// - Parameter context: A Dictionary of variables to be used by the
    ///                     template engine while rendering the template.
    /// - Parameter options: rendering options, specific per template engine
    ///
    /// - Returns: The content generated by rendering the template.
    /// - Throws: Any error thrown by the Templating Engine when it fails to
    ///          render the template.
    internal func render(template: String, context: [String: Any],
                         options: RenderingOptions = NullRenderingOptions()) throws -> String {
        let (optionalFileExtension, resourceWithExtension) = calculateExtension(template: template)
        
        let templateEngine = try getTemplateEngineForTemplate(template: template, optionalExtension: optionalFileExtension)
        let absoluteFilePath = buildAbsoluteFilePath(with: resourceWithExtension)
        
        return try templateEngine.render(filePath: absoluteFilePath, context: context, options: options,
                                         templateName: resourceWithExtension)
    }
    
    /// Render a template using an Encodable type. 
    /// - Parameter template: The path to the template file to be rendered.
    /// - Parameter value: A value which conforms to Encodable to be used by the
    ///                     template engine while rendering the template.
    /// - Parameter forKey: A value used to match the Encodable value to the correct variable in a template file.
    ///                                 The `forKey` value should match the desired variable in the template file.
    /// - Parameter options: rendering options, specific per template engine
    ///
    /// - Returns: The content generated by rendering the template.
    /// - Throws: Any error thrown by the Templating  Engine when it fails to
    ///          render the template.
    internal func render<T: Encodable>(template: String, with value: T, forKey key: String?,
                         options: RenderingOptions = NullRenderingOptions()) throws -> String {
        let (optionalFileExtension, resourceWithExtension) = calculateExtension(template: template)
        
        let templateEngine = try getTemplateEngineForTemplate(template: template, optionalExtension: optionalFileExtension)
        let absoluteFilePath = buildAbsoluteFilePath(with: resourceWithExtension)
        
        return try templateEngine.render(filePath: absoluteFilePath, with: value, forKey: key, options: options, templateName: resourceWithExtension)
    }
    
    private func getTemplateEngineForTemplate(template: String, optionalExtension: String?) throws -> TemplateEngine {
        // extension is nil (not the empty string), this should not happen
        guard let fileExtension = optionalExtension else {
            throw TemplatingError.noTemplateEngineForExtension(extension: "")
        }
        
        guard let templateEngine = getTemplateEngine(template: template) else {
            if fileExtension.isEmpty {
                throw TemplatingError.noDefaultTemplateEngineAndNoExtensionSpecified
            }
            
            throw TemplatingError.noTemplateEngineForExtension(extension: fileExtension)
        }
        return templateEngine
    }
    
    private func buildAbsoluteFilePath(with resourceWithExtension: String) -> String {
        let filePath: String
        if let decodedResourceExtension = resourceWithExtension.removingPercentEncoding {
            filePath = viewsPath + decodedResourceExtension
        } else {
            Log.warning("Unable to decode url \(resourceWithExtension)")
            filePath = viewsPath + resourceWithExtension
        }
        
        return StaticFileServer.ResourcePathHandler.getAbsolutePath(for: filePath)
    }

    func getTemplateEngine(template: String) -> TemplateEngine? {
        let (optionalFileExtension, _) = calculateExtension(template: template)

        guard let fileExtension = optionalFileExtension, !fileExtension.isEmpty else {
            return nil
        }

        return templateEngines[fileExtension]
    }

    // calculate file extension, consider default template engine extension
    private func calculateExtension(template: String) -> (fileExtension: String?, resourceWithExtension: String) {
        let fileExtension: String
        let resourceWithExtension: String

        guard let url = URL(string: template) else {
            return (fileExtension: nil, resourceWithExtension: template)
        }

        if url.pathExtension.isEmpty {
            fileExtension = defaultEngineFileExtension ?? ""
            
            resourceWithExtension = url.appendingPathExtension(fileExtension).absoluteString
        } else {
            fileExtension = url.pathExtension
            resourceWithExtension = template
        }

        return (fileExtension: fileExtension, resourceWithExtension: resourceWithExtension)
    }
    // MARK: Sub router
    
    /// Set up a "sub router" to handle requests. Chaining a route handler onto another router can make it easier to
    /// build a server that serves a large set of paths. Each sub router handles all of the path mappings below its
    /// parent's route path.
    /// ### Usage Example: ###
    /// The example below shows how the route `/parent/child' can be defined using a sub router.
    /// ```swift
    /// let router = Router()
    /// let parent = router.route("/parent")
    /// parent.get("/child") { request, response, next in
    ///     // If allowPartialMatch was set to false, this would not be called.
    /// }
    /// ```
    /// - Parameter route: The path to bind the sub router to.
    /// - Parameter mergeParameters: Specify if this router should have access to path parameters
    /// matched in its parent router. Defaults to `false` if not specified.
    /// - Parameter allowPartialMatch: Specify if the sub router allows a match when additional paths are added. In the example above, the `GET` request to `/parent/child` would only succeed if `allowPartialMatch` is set to `true`. Defaults to `true` if not specified.
    /// - Returns: The sub router which has been created.
    public func route(_ route: String, mergeParameters: Bool = false, allowPartialMatch: Bool = true) -> Router {
        let subrouter = Router(mergeParameters: mergeParameters)
        subrouter.parameterHandlers = self.parameterHandlers
        self.all(route, allowPartialMatch: allowPartialMatch, middleware: subrouter)
        return subrouter
    }

    // MARK: Parameter handling

    /// Set up handlers for a named request parameter. This can make it easier to handle
    /// multiple routes requiring the same parameter which needs to be handled in a certain way.
    /// ### Usage Example: ###
    /// ```swift
    /// let router = Router()
    /// router.parameter("id") { request, response, param, next in
    ///     if let _ = Int(param) {
    ///         // Id is an integer, continue
    ///         next()
    ///     }
    ///     else {
    ///         // Id is not an integer, error
    ///         try response.status(.badRequest).send("ID is not an integer").end()
    ///     }
    /// }
    ///
    /// router.get("/item/:id") { request, response, _ in
    ///     // This will only be reached if the id parameter is an integer
    /// }
    /// router.get("/user/:id") { request, response, _ in
    ///     // This will only be reached if the id parameter is an integer
    /// }
    /// ```
    ///
    /// - Parameter name: The single parameter name to be handled.
    /// - Parameter handler: The comma delimited set of `RouterParameterHandler` instances that will be
    ///                     invoked when request parses a parameter with the specified name.
    /// - Returns: The current router instance.
    @discardableResult
    public func parameter(_ name: String, handler: RouterParameterHandler...) -> Router {
        return self.parameter([name], handlers: handler)
    }

    /// Set up handlers for a number of named request parameters. This can make it easier to handle
    /// multiple routes requiring similar parameters which need to be handled in a certain way.
    /// ### Usage Example: ###
    /// ```swift
    /// let router = Router()
    /// router.parameter(["id", "num"]) { request, response, param, next in
    ///     if let _ = Int(param) {
    ///         // Parameter is an integer, continue
    ///         next()
    ///     }
    ///     else {
    ///         // Parameter is not an integer, error
    ///         try response.status(.badRequest).send("\(param) is not an integer").end()
    ///     }
    /// }
    ///
    /// router.get("/item/:id/:num") { request, response, _ in
    ///     // This will only be reached if the id and num parameters are integers.
    /// }
    /// ```
    ///
    /// - Parameter names: The array of parameter names to be handled.
    /// - Parameter handler: The comma delimited set of `RouterParameterHandler` instances that will be
    ///                     invoked when request parses a parameter with the specified name.
    /// - Returns: The current router instance.
    @discardableResult
    public func parameter(_ names: [String], handler: RouterParameterHandler...) -> Router {
        return self.parameter(names, handlers: handler)
    }

    /// Set up handlers for a number of named request parameters. This can make it easier to handle
    /// multiple routes requiring similar parameters which need to be handled in a certain way.
    /// ### Usage Example: ###
    /// ```swift
    /// let router = Router()
    /// func handleInt(request: RouterRequest, response: RouterResponse, param: String, next: @escaping () -> Void) throws -> Void {
    ///     if let _ = Int(param) {
    ///         // Parameter is an integer, continue
    ///     }
    ///     else {
    ///         // Parameter is not an integer, error
    ///         try response.status(.badRequest).send("\(param) is not an integer").end()
    ///     }
    ///     next()
    /// }
    ///
    /// func handleItem(request: RouterRequest, response: RouterResponse, param: String, next: @escaping () -> Void) throws -> Void {
    ///     let itemId = Int(param) //This will only be reached if id is an integer
    ///     ...
    /// }
    ///
    /// router.parameter(["id"], handlers: [handleInt, handleItem])
    ///
    /// router.get("/item/:id/") { request, response, _ in
    ///     ...
    /// }
    /// ```
    ///
    /// - Parameter names: The array of parameter names to be handled.
    /// - Parameter handlers: The array of `RouterParameterHandler` instances that will be
    ///                     invoked when request parses a parameter with the specified name.
    ///                     The handlers are executed in the order they are supplied.
    /// - Returns: The current router instance.
    @discardableResult
    public func parameter(_ names: [String], handlers: [RouterParameterHandler]) -> Router {
        for name in names {
            if self.parameterHandlers[name] == nil {
                self.parameterHandlers[name] = handlers
            } else {
                self.parameterHandlers[name]?.append(contentsOf: handlers)
            }
        }
        return self
    }
}


extension Router : RouterMiddleware {

    // MARK: RouterMiddleware extensions
    
    /// Handle an HTTP request as a middleware. Used internally in `Router` to allow for sub routing.
    ///
    /// - Parameter request: The `RouterRequest` object used to work with the incoming
    ///                     HTTP request.
    /// - Parameter response: The `RouterResponse` object used to respond to the
    ///                     HTTP request.
    /// - Parameter next: The closure called to invoke the next handler or middleware
    ///                     associated with the request.
    /// - Throws: Any `ErrorType`. If an error is thrown, processing of the request
    ///          is stopped, the error handlers, if any are defined, will be invoked,
    ///          and the user will get a response with a status code of 500.
    public func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) throws {
        guard let urlPath = request.parsedURLPath.path else {
            Log.error("request.parsedURLPath.path is nil. Failed to handle request")
            next()
            return
        }

        if request.allowPartialMatch {
            let mountpath = request.matchedPath

            /// Note: Since regex always start with ^, the beginning of line character,
            /// matched ranges always start at location 0, so it's OK to check via `hasPrefix`.
            /// Note: `hasPrefix("")` is `true` on macOS but `false` on Linux
            guard mountpath == "" || urlPath.hasPrefix(mountpath) else {
                Log.error("Failed to find matches in url")
                next()
                return
            }

            let index = urlPath.index(urlPath.startIndex, offsetBy: mountpath.count)
            request.parsedURLPath.path = String(urlPath[index...])
        }

        response.push(router: self)
        process(request: request, response: response) {
            request.parsedURLPath.path = urlPath
            response.popRouter()
            next()
        }
    }
}



extension Router : ServerDelegate {
    // MARK: HTTPServerDelegate extensions

    /// Handle new incoming requests to the server.
    ///
    /// - Parameter request: The `ServerRequest` object used to work with the incoming
    ///                     HTTP request at the [Kitura-net](http://ibm-swift.github.io/Kitura-net/) API level.
    /// - Parameter response: The `ServerResponse` object used to send responses to the
    ///                      HTTP request at the [Kitura-net](http://ibm-swift.github.io/Kitura-net/) API level.
    public func handle(request: ServerRequest, response: ServerResponse) {
        var decoder: (() -> BodyDecoder)?
        if let contentType = request.headers["Content-Type"]?[0], let mediaType = MediaType(contentTypeHeader: contentType) {
            decoder = decoders[mediaType]
        }
        let routeReq = RouterRequest(request: request, decoder: decoder?())
        //TODO fix the stack
        var routerStack = Stack<Router>()
        routerStack.push(self)
        let routeResp = RouterResponse(response: response, routerStack: routerStack, request: routeReq, encoders: encoders, defaultResponseMediaType: defaultResponseMediaType)

        process(request: routeReq, response: routeResp) { [weak self, weak routeReq, weak routeResp] () in
            guard let strongSelf = self else {
                Log.error("Found nil self at \(#file) \(#line)")
                return
            }
            guard let routeReq = routeReq else {
                Log.error("Found nil routeReq at \(#file) \(#line)")
                return
            }
            guard let routeResp = routeResp else {
                Log.error("Found nil routeResp at \(#file) \(#line)")
                return
            }
            do {
                if  !routeResp.state.invokedEnd {
                    if  routeResp.statusCode == .unknown  && !routeResp.state.invokedSend {
                        strongSelf.sendDefaultResponse(request: routeReq, response: routeResp)
                    }
                    if  !routeResp.state.invokedEnd {
                        try routeResp.end()
                    }
                }
            } catch {
                // Not much to do here
                Log.error("Failed to send response to the client")
            }
        }
    }

    /// Processes the request
    ///
    /// - Parameter request: The `RouterRequest` object used to work with the incoming
    ///                     HTTP request.
    /// - Parameter response: The `RouterResponse` object used to respond to the
    ///                     HTTP request.
    /// - Parameter next: The closure called to invoke the next handler or middleware
    ///                     associated with the request.
    fileprivate func process(request: RouterRequest, response: RouterResponse, callback: @escaping () -> Void) {
        guard let urlPath = request.parsedURLPath.path else {
            Log.error("request.parsedURLPath.path is nil. Failed to process request")
            callback()
            return
        }

        if  let fileResourceServer = fileResourceServer, urlPath.hasPrefix(kituraResourcePrefix) {
            let resource = String(urlPath[kituraResourcePrefix.endIndex...])
            fileResourceServer.sendIfFound(resource: resource, usingResponse: response)
        } else {
            let looper = RouterElementWalker(elements: self.elements,
                                             parameterHandlers: self.parameterHandlers,
                                             request: request,
                                             response: response,
                                             callback: callback)

            looper.next()
        }
    }

    /// Send default index.html file and its resources if appropriate, otherwise send
    /// default 404 message.
    ///
    /// - Parameter request: The `RouterRequest` object used to work with the incoming
    ///                     HTTP request.
    /// - Parameter response: The `RouterResponse` object used to respond to the
    ///                     HTTP request.
    private func sendDefaultResponse(request: RouterRequest, response: RouterResponse) {
        if let fileResourceServer = fileResourceServer, request.parsedURLPath.path == "/" {
            fileResourceServer.sendIfFound(resource: "index.html", usingResponse: response)
        } else {
            do {
                let errorMessage = "Cannot \(request.method) \(request.parsedURLPath.path ?? "")."
                try response.status(.notFound).send(errorMessage).end()
            } catch {
                Log.error("Error sending default not found message: \(error)")
            }
        }
    }
}