Fix: useFetch Failed to Hash Body in Nuxt

Error message:
Failed to hash body for caching: {error}
data fetching 2025-01-25

What Causes This Warning?

This warning appears when useFetch cannot create a unique cache key from the request body. This typically happens with non-serializable data like File objects, circular references, or complex objects.

The Problem

<script setup>
// ❌ Wrong - File objects can't be hashed
const file = ref<File | null>(null)
const { data } = await useFetch('/api/upload', {
  method: 'POST',
  body: { file: file.value }  // File can't be serialized for cache key
})

// ❌ Wrong - Circular reference
const obj = { name: 'test' }
obj.self = obj  // Circular!
const { data } = await useFetch('/api/data', {
  method: 'POST',
  body: obj
})

// ❌ Wrong - Function in body
const { data } = await useFetch('/api/data', {
  method: 'POST',
  body: {
    callback: () => console.log('test')  // Functions can't be serialized
  }
})
</script>

The Fix

Provide a Custom Key

<script setup>
const file = ref<File | null>(null)

// ✅ Correct - provide unique key manually
const { data } = await useFetch('/api/upload', {
  method: 'POST',
  body: { file: file.value },
  key: `upload-${file.value?.name}-${file.value?.size}`
})
</script>

Use $fetch for Non-Cacheable Requests

<script setup>
const file = ref<File | null>(null)

// ✅ Correct - use $fetch directly (no caching)
async function uploadFile() {
  const formData = new FormData()
  formData.append('file', file.value!)

  const result = await $fetch('/api/upload', {
    method: 'POST',
    body: formData
  })

  return result
}
</script>

Serialize Complex Objects

<script setup>
const complexData = ref({
  nested: { deeply: { value: 1 } },
  date: new Date()
})

// ✅ Correct - create serializable version for key
const serializedKey = computed(() =>
  JSON.stringify({
    nested: complexData.value.nested,
    date: complexData.value.date.toISOString()
  })
)

const { data } = await useFetch('/api/data', {
  method: 'POST',
  body: complexData,
  key: `data-${serializedKey.value}`
})
</script>

Common Scenarios

File Upload

<script setup>
const file = ref<File | null>(null)

// ✅ Best practice - use $fetch for file uploads
async function handleUpload() {
  if (!file.value) return

  const formData = new FormData()
  formData.append('file', file.value)

  try {
    const result = await $fetch('/api/upload', {
      method: 'POST',
      body: formData
    })
    console.log('Uploaded:', result)
  } catch (error) {
    console.error('Upload failed:', error)
  }
}
</script>

<template>
  <input type="file" @change="e => file = e.target.files?.[0]" />
  <button @click="handleUpload">Upload</button>
</template>

Dynamic Form Data

<script setup>
const form = reactive({
  name: '',
  email: '',
  items: []
})

// ✅ Use a computed key based on stable values
const formKey = computed(() => `form-${form.name}-${form.email}`)

const { data, execute } = useFetch('/api/submit', {
  method: 'POST',
  body: form,
  key: formKey,
  immediate: false
})

async function submitForm() {
  await execute()
}
</script>

With Timestamps

<script setup>
const payload = ref({
  data: 'some data',
  timestamp: new Date()
})

// ✅ Convert Date to string for hashing
const { data } = await useFetch('/api/data', {
  method: 'POST',
  body: computed(() => ({
    ...payload.value,
    timestamp: payload.value.timestamp.toISOString()
  })),
  key: `data-${payload.value.timestamp.getTime()}`
})
</script>

Avoiding Cache Entirely

<script setup>
// ✅ Disable caching for POST requests
const { data } = await useFetch('/api/action', {
  method: 'POST',
  body: { action: 'trigger' },
  getCachedData: () => null  // Never use cache
})
</script>

Understanding the Cache Key

Nuxt creates cache keys by hashing:

  • URL
  • Method
  • Query parameters
  • Request body (for POST/PUT/PATCH)

When the body can’t be hashed, you need to provide a manual key.

<script setup>
// How Nuxt builds the key internally (simplified)
const autoKey = computed(() => {
  return hash({
    url: '/api/data',
    method: 'POST',
    body: { /* serializable object */ }
  })
})

// When body isn't serializable, provide your own
const manualKey = computed(() => `my-unique-key-${someStableValue}`)
</script>

Best Practices

For Mutations, Use $fetch

<script setup>
// ✅ Mutations (POST/PUT/DELETE) often don't need caching
const createUser = async (userData) => {
  return await $fetch('/api/users', {
    method: 'POST',
    body: userData
  })
}

const updateUser = async (id, userData) => {
  return await $fetch(`/api/users/${id}`, {
    method: 'PUT',
    body: userData
  })
}
</script>

For Queries, Use useFetch

<script setup>
// ✅ Queries (GET) benefit from caching
const { data: users } = await useFetch('/api/users')

// With query params (auto-hashed)
const { data: filteredUsers } = await useFetch('/api/users', {
  query: { status: 'active' }
})
</script>

Hybrid Approach

<script setup>
// Query with useFetch (cached)
const { data: users, refresh } = await useFetch('/api/users')

// Mutation with $fetch (not cached)
async function addUser(userData) {
  await $fetch('/api/users', {
    method: 'POST',
    body: userData
  })
  // Refresh the cached query
  await refresh()
}
</script>

Quick Checklist

  • Check if body contains File, Blob, or non-serializable types
  • Use key option to provide manual cache key
  • Use $fetch directly for file uploads
  • Convert Date objects to strings
  • Remove functions/circular references from body
  • Consider getCachedData: () => null to disable caching