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 HandlerWarning: 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
Never store sensitive data in cookies without
httpOnly. Tokens, session IDs, and user identifiers should never be accessible via client-side JavaScript.Always set
Securein production. Without it, cookies can be intercepted over HTTP.Use
SameSite=Laxby default. Only useSameSite=Nonewhen you explicitly need cross-origin cookie support, and always combine it withSecure.Test cookie behavior across browsers. Safari has stricter cookie policies than Chrome, especially with cross-site tracking prevention.
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.