Middleware is used to alter the behavior of a request both before and after Outsmartly makes the request to your origin server and applies any overrides, if applicable.
The signature of a middleware is a function which accepts two arguments: an OutsmartlyMiddlewareEvent and a next() function. The OutsmartlyMiddlewareEvent contains additional information such as the OutsmartlyRequest object, OutsmartlyEdgeVisitor, helpers for cookies, and more.
Middleware should call next() whenever they want to continue on to the "next" middleware, or if it's the last middleware, continue to the default, built-in functionality of Outsmartly.
// a function which accepts an event object and a next() function.typeMiddleware= ( event:OutsmartlyMiddlewareEvent,next: (request?:Request) =>Promise<Response>,) =>PromiseOrValue<Response>;typePromiseOrValue<T> =Promise<T> |T;
It's a good practice to give your middleware functions names so that if there are any errors inside them, the error message seen in the browser response will contain the name of the middleware, to aid in debugging.
This pattern gives you the most flexibility: you can do things before, and after the default behavior. Not only that, you can also pass a different Request object when you call next(request) which then changes what request will actually be made.
The simplest middleware that does nothing (no-op) looks like this:
Middleware can be provided in you outsmartly.config.js in two places: at the top-level (applying all routes) or alternatively in a route itself (applying only paths that match.)
exportdefault { host:'example.outsmartly.app', environments: [ { name:'production', origin:'https://my-example-website.vercel.app', }, ],// All routes will have this middleware applied middleware: [functionfirstMiddleware(event, next) {returnnext(); }, ], routes: [ {// Any paths that start with /some-base-path/ will have// this middleware applied. path:'/some-base-path/*', middleware: [functionsecondMiddleware(event, next) {returnnext(); }, ], }, ],};
But that's not a very exciting piece of middleware. So let's see how we might add headers both to the request AND the response:
asyncfunctionauthorizationRedirectMiddleware(event, next) {// The request objects are not directly mutable, so we have to create our// own copy, using the existing headers and request as a base.constheaders=newHeaders(event.request.headers);headers.set('My-Custom-Request-Header','something');constrequest=newRequest(event.request, { headers, });// Move on to the next middleware, or built-in behavior. When this promise// resolves, we have already received the initial response from the origin// server (or the cache.)constresponse=awaitnext(request);response.headers.set('A-Different-Response-Header','another-thing');// If you wanted to, you could even return a totally different response.return response;}
You don't HAVE to call next(), but if you don't, remember that then Outsmartly will not make any requests to your origin or apply overrides to any HTML.
Examples
Set a cookie
Sometimes you want to set a cookie from the edge/server so that it can be an httpOnly cookie, which makes it inaccessible from client-side JavaScript in the browser (better security) and also is much more likely to survive longer without the browser or extensions deleting it.
Often this is inside an interceptor, but if you need to do this from middleware, it is also possible. Here's an example where we set a cookie the first time you land on a product page, so we can later tell to do things like personalization/recommendations.
exportdefault { host:'example.outsmartly.app', environments: [ { name:'production', origin:'https://my-example-website.vercel.app', }, ], routes: [ { path:'/products/:productName', middleware: [asyncfunctionlandPageProductNameMiddleware(event, next) {if (!event.cookies.has('myapp-landingPageProductName')) {// Get the product name so we can track whichconst { productName } =event.request.outsmartly.params;event.cookies.set('myapp-landingPageProductName', productName, { httpOnly:true, path:'/',// Expires in a year maxAge:60*60*24*7*52, }); }// Otherwise, defer to the default behavior.// It's important to call this if you don't need to redirect!returnawaitnext(); }, ], }, ],};
Redirects
In this example, for request paths that start with /app/ we check if they are authorized, and if not, we redirect them to the /login page.
This demonstrates the using the request's path from event.url.pathname, along with getting a cookie named 'session' by using the event.cookies utility.
functionisAuthorized(sessionCookieValue) {// Somehow decide whether or not they are authorized.// e.g. using JSON Web Tokensreturnfalse;}exportdefault { host:'example.outsmartly.app', environments: [ { name:'production', origin:'https://my-example-website.vercel.app', }, ], routes: [ { path:'/app/*', middleware: [asyncfunctionauthorizationRedirectMiddleware(event, next) {// Redirects work by providing a status and a Location header// to where you want to redirect the browser. No body is needed,// which is why we pass null.if (!isAuthorized(event.cookies.get('myapp-session'))) {returnnewResponse(null, { status:302, headers: { Location:'/login', }, }); }// Otherwise, defer to the default behavior.// It's important to call this if you don't need to redirect!returnawaitnext(); }, ], }, ],};