28.
Middleware
Written by Tanner Nelson
Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.
In the course of building your application, you’ll often find it necessary to integrate your own steps into the request pipeline. The most common mechanism for accomplishing this is to use one or more pieces of middleware. They allow you to do things like:
- Log incoming requests.
- Catch errors and display messages.
- Rate-limit traffic to particular routes.
Middleware instances sit between your router and the client connected to your server. This allows them to view, and potentially mutate, incoming requests before they reach your controllers. A middleware instance may choose to return early by generating its own response, or it can forward the request to the next responder in the chain. The final responder is always your router. When the response from the next responder is generated, the middleware can make any modifications it deems necessary, or choose to forward it back to the client as is. This means each middleware instance has control over both incoming requests and outgoing responses.
As you can see in the diagram above, the first middleware instance in your application — Middleware A — receives incoming requests from the client first. The first middleware may then choose to pass this request on to the next middleware — Middleware B — and so on.
Eventually, some component generates a response, which then traverses back through the middleware in the opposite direction. Take note that this means the first middleware receives responses last.
The protocol for Middleware is fairly simple, and should help you better understand the previous diagram:
public protocol Middleware {
func respond(
to request: Request,
chainingTo next: Responder) throws -> Future<Response>
}
In the case of Middleware A, request is the incoming data from the client, while next is Middleware B. The async response returned by Middleware A goes directly to the client.
For Middleware B, request is the request passed on from Middleware A. next is the router. The future response returned by Middleware B goes to Middleware A.
Vapor’s middleware
Vapor includes some middleware out of the box. This section introduces you to the available options to give you an idea of what middleware is commonly used for.
Error middleware
The most commonly used middleware in Vapor is ErrorMiddleware. It’s responsible for converting both synchronous and asynchronous Swift errors into HTTP responses. Uncaught errors cause the HTTP server to immediately close the connection and print an internal error log.
Using the ErrorMiddleware ensures all errors you throw are rendered into appropriate HTTP responses.
In production mode, ErrorMiddleware converts all errors into opaque 500 Internal Server Error responses. This is important for keeping your application secure, as errors may contain sensitive information.
You can opt into providing different error responses by conforming your error types to AbortError, allowing you to specify the HTTP status code and error message. You may also use Abort, a concrete error type that conforms to AbortError. For example:
throw Abort(.badRequest, "Something's not quite right.")
File middleware
Another common type of middleware is FileMiddleware. This middleware serves files from the Public folder in your application directory. This is useful when you’re using Vapor to create a front-end website that may require static files like images or stylesheets.
Other Middleware
Vapor also provides a SessionsMiddleware, responsible for tracking sessions with connected clients. Other packages may provide middleware to help them integrate into your application. For example, Vapor’s Authentication package contains middleware for protecting your routes using basic passwords, simple bearer tokens, and even JWTs (JSON Web Tokens).
Example: Todo API
Now that you have an understanding of how various types of middleware function, you’re ready to learn how to configure them and how to create your own custom middleware types.
To do this, you’ll implement a basic Todo list API. This API has three routes:
$ swift run Run routes
+--------+--------------+
| GET | /todos |
+--------+--------------+
| POST | /todos |
+--------+--------------+
| DELETE | /todos/:todo |
+--------+--------------+
You’ll create and configure two different middleware types for this project:
-
LogMiddleware: Logs response times for incoming requests. -
SecretMiddleware: Protects private routes from being accessed without permission by requiring a secret key.
Log middleware
The first middleware you’ll create will log incoming requests. It will display the following information for each request:
- Request method
- Request path
- Response status
- How long it took to generate the response
Open the starter project directory in Terminal and generate an Xcode project for it by entering:
vapor xcode -y
Once Xcode opens, navigate to Middleware/LogMiddleware.swift. There you’ll find an empty LogMiddleware class.
Ignore the TimeInterval extension for now; you’ll use that later.
Start by conforming LogMiddleware to the Middleware protocol. Only one method is required: respond(to:chainingTo:).
For now, the middleware will just log the incoming request’s description. Replace LogMiddleware with the following:
final class LogMiddleware: Middleware {
// 1
let logger: Logger
init(logger: Logger) {
self.logger = logger
}
// 2
func respond(
to req: Request,
chainingTo next: Responder) throws -> Future<Response> {
// 3
logger.info(req.description)
// 4
return try next.respond(to: req)
}
}
// 5
extension LogMiddleware: ServiceType {
static func makeService(
for container: Container) throws -> LogMiddleware {
// 6
return try .init(logger: container.make())
}
}
Here’s a breakdown of the code you just added:
- Create a stored property to hold a
Logger. - Implement the
Middlewareprotocol requirement. - Send the request’s description to the Logger as an informational log.
- Forward the incoming request to the next responder.
- Allow
LogMiddlewareto be registered as a service in your application. - Initialize an instance of
LogMiddleware, using the container to create the necessaryLogger.
Now that you’ve created a custom middleware, you need to register it to your application. Open configure.swift and add the following line to under // register custom service types here:
services.register(LogMiddleware.self)
Once LogMiddleware is registered, you can use MiddlewareConfig to integrate it. Next, add the following line under var middleware = MiddlewareConfig():
middleware.use(LogMiddleware.self)
This enables LogMiddleware globally. The ordering is important here as well: Since LogMiddleware is added before ErrorMiddleware, it receives requests first and responses last. This ensures that LogMiddleware logs the original request from the client unmodified by other middleware and the final response right before it goes out to the client.
Finally, build and run your application, then make a request to GET /todos using curl:
curl localhost:8080/todos
Take a look at the log output from your running application. You’ll see something similar to:
[ INFO ] GET /todos HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.54.0
Accept: */*
<no body> (LogMiddleware.swift:15)
This is a great start! But you can improve LogMiddleware to provide more useful, readable output. Open LogMiddleware.swift and replace the implementation of respond(to:chainingTo:) with the following methods:
func respond(
to req: Request,
chainingTo next: Responder) throws -> Future<Response> {
// 1
let start = Date()
return try next.respond(to: req).map { res in
// 2
self.log(res, start: start, for: req)
return res
}
}
// 3
func log(_ res: Response, start: Date, for req: Request) {
let reqInfo = "\(req.http.method.string) \(req.http.url.path)"
let resInfo = "\(res.http.status.code) " +
"\(res.http.status.reasonPhrase)"
// 4
let time = Date()
.timeIntervalSince(start)
.readableMilliseconds
// 5
logger.info("\(reqInfo) -> \(resInfo) [\(time)]")
}
Here’s a breakdown of how the new methods work:
- First, create a start time. Do this before any additional work is done to get the most accurate response time measurement.
- Instead of returning the response directly, map the future result so that you can access the
Responseobject. Pass this tolog(_:start:for:). - This method logs the response for an incoming request using the response start date.
- Generate a readable time using
timeIntervalSince(_:)and the extension onTimeIntervalat the bottom of the file. - Log the information string.
Now that you’ve updated LogMiddleware, build and run and curl GET /todos again.
curl localhost:8080/todos
If you check the output of your application, you’ll see a new, more concise output format.
[ INFO ] GET /todos -> 200 OK [1.9ms] (LogMiddleware.swift:32)
Secret middleware
Now that you’ve learned how to create middleware and apply it globally, you’ll learn how to apply middleware to specific routes.
Two of the Todo List APIs routes can make changes to the database:
- POST /todos
- DELETE /todos/:id
If this were a public API, you’d want to protect these routes with a secret key using middleware. That’s exactly what SecretMiddleware will do.
Open Middleware/SecretMiddleware.swift and replace the class definition of SecretMiddleware with the following code:
final class SecretMiddleware: Middleware {
// 1
let secret: String
init(secret: String) {
self.secret = secret
}
// 2
func respond(
to request: Request,
chainingTo next: Responder) throws -> Future<Response> {
// 3
guard
request.http.headers.firstValue(name: .xSecret) == secret
else {
// 4
throw Abort(
.unauthorized,
reason: "Incorrect X-Secret header.")
}
// 5
return try next.respond(to: request)
}
}
Here’s a breakdown of how SecretMiddleware works:
- Create a stored property to hold the secret key.
- Implement
Middlewareprotocol requirement. - Check the X-Secret header in the incoming request against the configured secret key.
- If the header value does not match, throw an error with
unauthorizedHTTP status. - If the header matches, chain to the next middleware normally.
Now you just need to conform SecretMiddleware to ServiceType so that it can be used as a service in your application.
Add the following code after the SecretMiddleware implementation.
extension SecretMiddleware: ServiceType {
static func makeService(
for worker: Container) throws -> SecretMiddleware {
// 1
let secret: String
switch worker.environment {
// 2
case .development: secret = "foo"
default:
// 3
guard let envSecret = Environment.get("SECRET") else {
let reason = """
No $SECRET set on environment. \
Use "export SECRET=<secret>"
"""
throw Abort(
.internalServerError,
reason: reason)
}
secret = envSecret
}
// 4
return SecretMiddleware(secret: secret)
}
}
Here’s a breakdown of how this code works:
- Create a local variable to store the configured secret key.
- If the current environment is development, just use foo as the key.
- If the current environment is not development, attempt to fetch the key from the process environment at key $SECRET.
- Initialize an instance of
SecretMiddlewareusing the configured key.
Time to register the new middleware. Open configure.swift and add the following under the comment // register custom service types.
services.register(SecretMiddleware.self)
Now you’ve created and registered SecretMiddleware, you can use it to protect the desired routes. Open routes.swift and replace the POST and DELETE routes with the following code:
// 1
router.group(SecretMiddleware.self) { secretGroup in
// 2
secretGroup.post("todos", use: todoController.create)
secretGroup.delete(
"todos",
Todo.parameter,
use: todoController.delete)
}
Here’s what this does:
- Create a new route group wrapped by
SecretMiddleware. - Register the POST and DELETE routes in the newly created route group instead of the global router.
Build and run the application, then create a new request in RESTed. Configure the request as follows:
- URL: http://localhost:8080/todos
- method: POST
Add a parameter with name and value:
- title: This is a test TODO!
Click Send Request and notice the response:
{
"error": true,
"reason": "Incorrect X-Secret header."
}
The middleware is protecting the routes! If you try querying GET /todos you’ll notice it still works.
Add X-Secret: foo to the headers section in RESTed and send the request again. Now you’ll notice that the response has changed. The middleware is allowing this request through to the controller now it has the appropriate headers.
Where to go from here?
Middleware is extremely useful for creating large web applications. It allows you to apply restrictions and transformations globally or to just a few routes using discrete, re-usable components. In this chapter, you learned how to create a global LogMiddleware that displayed information about all incoming requests to your app. You then created SecretMiddleware, which could protect select routes from public access.
For more information about using middleware, be sure to check out Vapor’s API Docs: