Oct 05, 2022
Colin Sidoti
Compute a value in middleware and pass it to your API route or getServerSideProps. Works in both Node and Edge runtimes.
Unlike many web frameworks, Next.js middleware
Instead, middleware has a feature called "rewrites" that we can leverage to pass data.
The high-level idea is to "rewrite" requests to the same URL, but modify the request's metadata to include the data we want to pass. Then, we can read the metadata from our API route or getServerSideProps.
Unfortunately, there are inconsistencies across runtimes that make it difficult to get this working. We hope these snippets help save you save you some headaches.
License: MIT
Usage: middleware.js
import { NextResponse } from "next/server";import { withContext } from "./context";// Pre-define the possible context keys to prevent spoofingconst allowedContextKeys = ["foo"];export default withContext(allowedContextKeys, (setContext, req) => {setContext("foo", "bar");return NextResponse.next();});
Usage: API route (Node)
import { getContext } from "../../context";export default function handler(req, res) {res.status(200).json({ foo: getContext(req, "foo") });}
Usage API route (Edge)
import { getContext } from "../../context";export default function handler(req) {return new Response(JSON.stringify({ foo: getContext(req, "foo") }));}
Usage: getServerSideProps (Edge and Node)
import { getContext } from "../context";export const getServerSideProps = ({ req }) => {return { props: { foo: getContext(req, "foo") } };};
Source: (saved to context.js on your root)
import { NextResponse } from "next/server";const ctxKey = (key) => `ctx-${key.toLowerCase()}`;export const getContext = (req, rawKey) => {const key = ctxKey(rawKey);let headerValue =typeof req.headers.get === "function"? req.headers.get(key) // Edge: req.headers[key]; // Node;// Necessary for node in development environmentif (!headerValue) {headerValue = req.socket?._httpMessage?.getHeader(key);}if (headerValue) {return headerValue;}// Use a dummy url because some environments only return// a path, not the full urlconst reqURL = new URL(req.url, "http://dummy.url");return reqURL.searchParams.get(key);};export const withContext = (allowedKeys, middleware) => {// Normalize allowed keysfor (let i = 0; i < allowedKeys.length; i++) {if (typeof allowedKeys[i] !== "string") {throw new Error("All keys must be strings");}allowedKeys[i] = ctxKey(allowedKeys[i]);}return (req, evt) => {const reqURL = new URL(req.url);// First, make sure allowedKeys aren't being spoofed.// Reliably overriding spoofed keys is a tricky problem and// different hosts may behave different behavior - it's best// just to safelist "allowedKeys" and block if they're being// spoofedfor (const allowedKey of allowedKeys) {if (req.headers.get(allowedKey) || reqURL.searchParams.get(allowedKey)) {throw new Error(`Key ${allowedKey.substring(4)} is being spoofed. Blocking this request.`);}}const data = {};const setContext = (rawKey, value) => {const key = ctxKey(rawKey);if (!allowedKeys.includes(key)) {throw new Error(`Key ${rawKey} is not allowed. Add it to withContext's first argument.`);}if (typeof value !== "string") {throw new Error(`Value for ${rawKey} must be a string, received ${typeof value}`);}data[key] = value;};let res = middleware(setContext, req, evt) || NextResponse.next();// setContext wasn't called, passthroughif (Object.keys(data).length === 0) {return res;}// Don't modify redirectsif (res.headers.get("Location")) {return res;}const rewriteURL = new URL(res.headers.get("x-middleware-rewrite") || req.url);// Don't modify cross-origin rewritesif (reqURL.origin !== rewriteURL.origin) {return res;}// Set context directly on the res object (headers)// and on the rewrite url (query string)for (const key in data) {res.headers.set(key, data[key]);rewriteURL.searchParams.set(key, data[key]);}// set the updated rewrite urlres.headers.set("x-middleware-rewrite", rewriteURL.href);return res;};};
Known limitations:
Depending on the runtime, your data will be transmitted as either an HTTP header or a URL query string. This leads to several limitations:
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.