Fix: Session Expired (H3) in Nuxt

Error message:
Session expired. Please refresh.
ssr 2025-01-25

What Causes This Error?

This error occurs in H3 (Nuxt’s server framework) when a session has expired or is no longer valid. This typically happens with server-side session management.

Common Causes

  1. Session TTL (time-to-live) exceeded
  2. Session storage cleared
  3. Server restart (in-memory sessions)
  4. Invalid session ID
  5. Cookie expired before session

The Fix

Configure Session Properly

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!,
    maxAge: 60 * 60 * 24 * 7,  // 7 days
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax'
    }
  })

  await session.update({
    userId: 'user-123',
    createdAt: Date.now()
  })

  return { success: true }
})

Handle Expired Sessions

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  try {
    const session = await useSession(event, {
      password: process.env.SESSION_SECRET!
    })

    // Check if session exists and is valid
    if (!session.data.userId) {
      // Session expired or invalid
      return  // Allow request to continue without auth
    }

    // Attach user to event context
    event.context.auth = {
      userId: session.data.userId
    }
  } catch (error) {
    // Session error - clear it
    console.warn('Session error:', error)
  }
})

Refresh Session Before Expiry

// server/api/auth/refresh.post.ts
export default defineEventHandler(async (event) => {
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!,
    maxAge: 60 * 60 * 24 * 7
  })

  if (!session.data.userId) {
    throw createError({
      statusCode: 401,
      message: 'Session expired. Please login again.'
    })
  }

  // Refresh session by updating
  await session.update({
    ...session.data,
    lastAccess: Date.now()
  })

  return { success: true }
})

Session Storage

In-Memory (Default - Not for Production)

// Sessions lost on server restart
const session = await useSession(event, {
  password: process.env.SESSION_SECRET!
})

With Redis (Production)

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    storage: {
      sessions: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  }
})
// server/api/auth/session.ts
export default defineEventHandler(async (event) => {
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!,
    name: 'session',
    cookie: {
      maxAge: 60 * 60 * 24 * 7
    }
  })

  return { user: session.data }
})

Client-Side Handling

<script setup>
const { data, error, refresh } = await useFetch('/api/user')

watch(error, (err) => {
  if (err?.statusCode === 401) {
    // Session expired - redirect to login
    navigateTo('/login?expired=true')
  }
})

// Periodic session refresh
onMounted(() => {
  const interval = setInterval(async () => {
    try {
      await $fetch('/api/auth/refresh', { method: 'POST' })
    } catch (e) {
      // Session expired
      navigateTo('/login')
    }
  }, 15 * 60 * 1000)  // Every 15 minutes

  onUnmounted(() => clearInterval(interval))
})
</script>

API Middleware for Auth

// server/utils/requireAuth.ts
export async function requireAuth(event: H3Event) {
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!
  })

  if (!session.data.userId) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Session expired',
      message: 'Please login to continue'
    })
  }

  return session.data
}
// server/api/protected/data.ts
export default defineEventHandler(async (event) => {
  const user = await requireAuth(event)

  return {
    message: `Hello ${user.userId}`
  }
})

Session Configuration Options

await useSession(event, {
  // Required - encrypt session data
  password: process.env.SESSION_SECRET!,  // Min 32 chars

  // Session name (cookie name)
  name: 'my-session',

  // Session lifetime
  maxAge: 60 * 60 * 24,  // 24 hours in seconds

  // Cookie options
  cookie: {
    httpOnly: true,      // Not accessible via JS
    secure: true,        // HTTPS only
    sameSite: 'lax',     // CSRF protection
    path: '/',           // Cookie path
    domain: '.example.com'  // Cookie domain
  }
})

Clearing Sessions

// server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!
  })

  // Clear session data
  await session.clear()

  return { success: true }
})

Quick Checklist

  • SESSION_SECRET is at least 32 characters
  • maxAge is set appropriately
  • Use persistent storage (Redis) in production
  • Handle 401 errors on client side
  • Implement session refresh mechanism
  • Clear sessions properly on logout
  • Cookie settings match your security requirements