How to pass a value from Next.js middleware to API routes and getServerSideProps

Category
Engineering
Published

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 doesn't have a built-in mechanism for passing values from middleware to other parts of the application.

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

Live Demo | Source Code

Usage: middleware.js

import { NextResponse } from 'next/server'
import { withContext } from './context'

// Pre-define the possible context keys to prevent spoofing
const 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 environment
  if (!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 url
  const reqURL = new URL(req.url, 'http://dummy.url')

  return reqURL.searchParams.get(key)
}

export const withContext = (allowedKeys, middleware) => {
  // Normalize allowed keys
  for (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
    // spoofed
    for (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, passthrough
    if (Object.keys(data).length === 0) {
      return res
    }

    // Don't modify redirects
    if (res.headers.get('Location')) {
      return res
    }

    const rewriteURL = new URL(res.headers.get('x-middleware-rewrite') || req.url)

    // Don't modify cross-origin rewrites
    if (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 url
    res.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:

  • Headers and query strings only accept strings for key/value pairs. If you'd like to use non-strings, you'll need to bring your own serializer
  • Keys are lowercases because headers are case-insensitive
  • Your host likely limits the total overall length of headers and query strings. Here are the limits for Vercel's edge runtime, for example
Author
Colin Sidoti