Fix: Cannot Update Read-Only Session in Nuxt

Error message:
Cannot update read-only session data.
ssr 2025-01-25

What Causes This Error?

This error occurs when you try to modify session data directly instead of using the proper session.update() method. H3 sessions are designed to be immutable for safety.

The Problem

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

  // ❌ Wrong - direct modification
  session.data.userId = 'new-id'
  session.data.name = 'John'

  // ❌ Wrong - object spread doesn't persist
  Object.assign(session.data, { userId: 'new-id' })
})

The Fix

Use session.update()

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

  // ✅ Correct - use update method
  await session.update({
    userId: 'new-id',
    name: 'John'
  })

  return { success: true }
})

Partial Updates

// server/api/update-name.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!
  })

  // ✅ Merge with existing data
  await session.update({
    ...session.data,
    name: body.name
  })

  return { success: true }
})

Common Patterns

Login

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event)

  // Validate credentials...
  const user = await validateUser(email, password)

  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!
  })

  // ✅ Set session data
  await session.update({
    userId: user.id,
    email: user.email,
    role: user.role,
    loginAt: Date.now()
  })

  return { user: { id: user.id, email: user.email } }
})

Update Profile

// server/api/profile.put.ts
export default defineEventHandler(async (event) => {
  const updates = await readBody(event)
  const session = await useSession(event, {
    password: process.env.SESSION_SECRET!
  })

  if (!session.data.userId) {
    throw createError({ statusCode: 401 })
  }

  // Update user in database...

  // ✅ Update session with new data
  await session.update({
    ...session.data,
    email: updates.email ?? session.data.email,
    name: updates.name ?? session.data.name,
    updatedAt: Date.now()
  })

  return { success: true }
})

Logout

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

  // ✅ Clear session completely
  await session.clear()

  return { success: true }
})

Increment Counter

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

  // ✅ Update with new value
  const currentCount = session.data.visits || 0
  await session.update({
    ...session.data,
    visits: currentCount + 1,
    lastVisit: Date.now()
  })

  return { visits: currentCount + 1 }
})

Session Helper Utility

// server/utils/session.ts
import type { H3Event } from 'h3'

interface SessionData {
  userId?: string
  email?: string
  role?: string
  [key: string]: unknown
}

export async function getSession(event: H3Event) {
  return await useSession<SessionData>(event, {
    password: process.env.SESSION_SECRET!,
    maxAge: 60 * 60 * 24 * 7
  })
}

export async function setSessionData(
  event: H3Event,
  data: Partial<SessionData>
) {
  const session = await getSession(event)
  await session.update({
    ...session.data,
    ...data
  })
  return session.data
}

export async function clearSession(event: H3Event) {
  const session = await getSession(event)
  await session.clear()
}

Usage:

// server/api/example.ts
export default defineEventHandler(async (event) => {
  await setSessionData(event, { role: 'admin' })
  return { success: true }
})

Reading Session (No Error)

Reading session data doesn’t require update:

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

  // ✅ Reading is fine
  if (!session.data.userId) {
    throw createError({ statusCode: 401 })
  }

  return {
    userId: session.data.userId,
    email: session.data.email
  }
})

TypeScript Interface

// types/session.d.ts
declare module 'h3' {
  interface SessionData {
    userId?: string
    email?: string
    role?: 'user' | 'admin'
    loginAt?: number
  }
}

Quick Checklist

  • Use session.update({...}) to modify data
  • Don’t assign directly to session.data.*
  • Spread existing data for partial updates
  • Use session.clear() to remove all data
  • Reading session.data is safe without update
  • Create helper utilities for cleaner code