nextjs-middleware-vitest-jsdom

star 0

Fix "request.headers must be an instance of Headers" error when testing Next.js middleware with Vitest in jsdom environment. Use when: (1) NextResponse.next() throws "request.headers must be an instance of Headers" in vitest/jsdom, (2) middleware integration tests fail despite NextRequest being constructed correctly, (3) testing locale detection, redirect logic, or cookie handling in Next.js middleware without edge runtime. Covers mocking NextResponse.next() and NextResponse.redirect() to bypass the jsdom/edge-runtime Headers class mismatch while preserving testable .status, .headers, and .cookies.set().

hubeiqiao By hubeiqiao schedule Updated 3/4/2026

name: nextjs-middleware-vitest-jsdom description: | Fix "request.headers must be an instance of Headers" error when testing Next.js middleware with Vitest in jsdom environment. Use when: (1) NextResponse.next() throws "request.headers must be an instance of Headers" in vitest/jsdom, (2) middleware integration tests fail despite NextRequest being constructed correctly, (3) testing locale detection, redirect logic, or cookie handling in Next.js middleware without edge runtime. Covers mocking NextResponse.next() and NextResponse.redirect() to bypass the jsdom/edge-runtime Headers class mismatch while preserving testable .status, .headers, and .cookies.set(). author: Claude Code version: 1.0.0 date: 2026-02-16 tags: Next.js, middleware, vitest, jsdom, Headers, NextResponse, testing, i18n

Next.js Middleware Testing in Vitest/jsdom

Problem

When testing Next.js middleware with Vitest using the jsdom environment, NextResponse.next({ request: { headers: request.headers } }) throws:

Error: request.headers must be an instance of Headers
 at handleMiddlewareField node_modules/next/src/server/web/spec-extension/response.ts:18:13
 at Function.next node_modules/next/src/server/web/spec-extension/response.ts:150:5

Context / Trigger Conditions

  • Testing middleware.ts that calls NextResponse.next() or NextResponse.redirect()
  • Using Vitest with environment: 'jsdom' (common default for Next.js projects)
  • NextRequest constructs fine, but NextResponse.next() fails
  • The error occurs at the instanceof Headers check inside Next.js internals

Root Cause

Next.js middleware runs in the edge runtime, which uses its own Headers class (from undici / Node.js built-in). When testing in jsdom, the Headers class is jsdom's polyfill. Since these are different classes, the instanceof check in NextResponse.next() fails — even though the headers object is functionally identical.

This is NOT a bug in your code. It's a runtime environment mismatch between jsdom and Next.js's edge runtime internals.

Solution

Mock NextResponse (but keep NextRequest real) to bypass the internal Headers check. The mock provides minimal response objects with .status, .headers, and .cookies.set().

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';

// Mock NextResponse to bypass edge-runtime Headers mismatch in jsdom
vi.mock('next/server', async (importOriginal) => {
  const actual = await importOriginal<typeof import('next/server')>();

  function createMockResponse(init?: { status?: number; headers?: Record<string, string> }) {
    const headers = new Headers(init?.headers);
    const status = init?.status ?? 200;

    return {
      status,
      headers,
      cookies: {
        set(
          nameOrObj: string | { name: string; value: string; [k: string]: unknown },
          value?: string,
        ) {
          if (typeof nameOrObj === 'string') {
            headers.append('set-cookie', `${nameOrObj}=${value}`);
          } else {
            headers.append('set-cookie', `${nameOrObj.name}=${nameOrObj.value}`);
          }
        },
      },
    };
  }

  const MockNextResponse = Object.assign(
    function MockNextResponse() {},
    {
      next: () => createMockResponse(),
      redirect: (url: URL | string, status?: number) => {
        const location = url instanceof URL ? url.toString() : url;
        return createMockResponse({ status: status ?? 307, headers: { location } });
      },
    },
  );

  return {
    ...actual,
    NextResponse: MockNextResponse,
  };
});

// Import middleware AFTER mocking
import { middleware } from '@/middleware';

Key Design Decisions

  1. importOriginal: Keeps NextRequest real so you can test headers, cookies, and URL parsing without additional mocking.

  2. cookies.set() writes to headers: The mock appends set-cookie headers so tests can verify cookie behavior via res.headers.get('set-cookie').

  3. Default status 307 for redirects: Matches Next.js's default redirect behavior. Pass an explicit status (e.g., 301) for permanent redirects.

Testing Patterns

// Redirect assertions
const res = await middleware(req);
expect(res.status).toBe(307);
expect(res.headers.get('location')).toContain('/zh-CN');
expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=zh-CN');

// Passthrough assertions
const res = await middleware(req);
expect(res.status).toBe(200);
expect(res.headers.get('location')).toBeNull();

Verification

After applying the mock:

  1. Pure function tests (exported from middleware) should work without any mocking
  2. Full middleware integration tests should pass without "instanceof Headers" errors
  3. Redirect status codes, location headers, and cookie behavior are all testable

Notes

  • If your middleware uses response.cookies.get() or response.cookies.delete(), extend the mock's cookies object accordingly.
  • If you also mock @supabase/ssr (common for auth middleware), place that mock alongside the next/server mock, before the middleware import.
  • This mock is intentionally minimal — it only covers what middleware tests typically assert. Extend as needed for your specific middleware logic.
  • An alternative approach is to use @edge-runtime/jest-environment or vitest-environment-edge-runtime, but these require additional setup and may conflict with jsdom-dependent tests elsewhere in the suite.

References

Install via CLI
npx skills add https://github.com/hubeiqiao/skills --skill nextjs-middleware-vitest-jsdom
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator