security2024-09-30·11 min·266/348

Next.js Middleware Cookie Configuration Errors and Fixes

How to properly set, read, and manipulate cookies in Next.js Middleware without causing errors or security issues

Next.js Middleware Cookie Configuration Errors and Fixes

Introduction

Middleware in Next.js runs before a request is completed. It is the right place to handle authentication redirects, locale detection, and A/B testing. A common use case is setting cookies to persist user preferences or session tokens. However, working with cookies in Middleware introduces several subtle issues that can break your application or create security vulnerabilities.

After dealing with a production incident where a cookie configuration bug exposed user data, I documented every cookie-related issue in Middleware and the correct way to handle each one.

Environment

  • OS: Windows 11
  • Node.js: v20.10.0
  • Next.js: 14.0.4
  • Runtime: Edge Runtime (Middleware default)

Problem

Cookie operations in Middleware produced several distinct errors:

TypeError: Cannot read properties of undefined (reading 'get')
Error:.cookies() can only be called in a Server Component or Route Handler
Warning: Setting cookies in Middleware is not recommended for performance.
Use Set-Cookie header instead.
Error: The cookie name contains invalid characters: "="

In production, the cookie was set but was not available in subsequent requests:

# Cookie sent in response headers
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax

# But cookie not present in next request
Cookie: (empty)

Analysis

The root causes were:

1. Wrong API for cookie access. The cookies() function from next/headers is server-only and cannot be used in Middleware. Middleware uses request.cookies and response.cookies instead.

2. Cookie serialization issues. Special characters in cookie values must be encoded. The Next.js Middleware cookie API handles this automatically for .set(), but manual header manipulation requires encoding.

3. Domain and path configuration. If the cookie path or domain does not match the current request, the browser will not send it.

4. Secure flag without HTTPS. Setting Secure on cookies in development (HTTP) prevents the browser from storing them.

Solution

Fix 1: Correct Middleware cookie access

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  // READ cookies from the request
  const session = request.cookies.get('session')
  const locale = request.cookies.get('locale')

  // CREATE response
  const response = NextResponse.next()

  // SET cookies on the response
  if (!session) {
    response.cookies.set('session', generateSessionId(), {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      path: '/',
      maxAge: 60 * 60 * 24 * 30, // 30 days
    })
  }

  // SET a preference cookie
  if (!locale) {
    response.cookies.set('locale', 'en', {
      path: '/',
      maxAge: 60 * 60 * 24 * 365, // 1 year
    })
  }

  return response
}

function generateSessionId(): string {
  return Array.from(crypto.getRandomValues(new Uint8Array(32)))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

Fix 2: Secure cookie attributes

// lib/cookieConfig.ts
export function getSecureCookieConfig() {
  const isProduction = process.env.NODE_ENV === 'production'

  return {
    httpOnly: true,
    secure: isProduction,
    sameSite: 'lax' as const,
    path: '/',
    // Do not set domain - let the browser use the current domain
  }
}

export function getSessionCookieConfig() {
  return {
    ...getSecureCookieConfig(),
    maxAge: 60 * 60 * 24 * 7, // 7 days
  }
}

export function getPreferenceCookieConfig() {
  return {
    ...getSecureCookieConfig(),
    maxAge: 60 * 60 * 24 * 365, // 1 year
    httpOnly: false, // Allow client-side access for preferences
  }
}

Fix 3: Remove cookies in Middleware

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()

  // Check for expired session
  const session = request.cookies.get('session')
  if (session && isSessionExpired(session.value)) {
    // Remove the expired cookie
    response.cookies.delete('session')
    response.cookies.delete('userId')

    // Redirect to login
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return response
}

function isSessionExpired(sessionId: string): boolean {
  // Parse session expiry from your session store
  // This is a simplified example
  try {
    const payload = JSON.parse(atob(sessionId.split('.')[1]))
    return payload.exp * 1000 < Date.now()
  } catch {
    return true
  }
}

Fix 4: Cross-origin cookie handling

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()

  // For cross-origin requests (e.g., from www.example.com to api.example.com)
  response.cookies.set('csrf-token', generateCsrfToken(), {
    httpOnly: false, // Must be readable by client-side JS
    secure: true,
    sameSite: 'none', // Required for cross-origin
    path: '/',
  })

  // CORS headers for API routes
  if (request.nextUrl.pathname.startsWith('/api')) {
    response.headers.set('Access-Control-Allow-Origin', request.headers.get('origin') || '')
    response.headers.set('Access-Control-Allow-Credentials', 'true')
  }

  return response
}

Lessons Learned

  1. Never store sensitive data in cookies without httpOnly. Tokens, session IDs, and user identifiers should never be accessible via client-side JavaScript.

  2. Always set Secure in production. Without it, cookies can be intercepted over HTTP.

  3. Use SameSite=Lax by default. Only use SameSite=None when you explicitly need cross-origin cookie support, and always combine it with Secure.

  4. Test cookie behavior across browsers. Safari has stricter cookie policies than Chrome, especially with cross-site tracking prevention.

  5. Prefer server-side session storage over client-side cookies for sensitive data. Use cookies only for non-sensitive preferences and session identifiers.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.