Function Signature
retrieveFileLocally(
event: H3Event<EventHandlerRequest>,
filename: string,
filelocation?: string
): Promise<NodeJS.ReadableStream>
Description
Retrieves a file from local storage and returns it as a readable stream for sending to the client. The function automatically sets appropriate HTTP headers including content-type, content-length, and content-disposition.
Parameters
event
H3Event<EventHandlerRequest>
required
The H3 event object from your Nuxt/Nitro event handler. This is used to set response headers.
The name of the file to retrieve. This should be the filename returned by storeFileLocally.
- Must be a safe basename (no path separators)
- Example:
"aBcD1234.png"
The folder path where the file is located, relative to the configured mount point.
- Use forward slashes for subdirectories (e.g.,
/userFiles or /uploads/images)
- Defaults to the root of the mount directory if not provided
- Path is automatically normalized and validated for security
Return Value
stream
Promise<NodeJS.ReadableStream>
A readable stream of the file contents. The stream is automatically handled by Nuxt/Nitro and sent to the client.
The function automatically sets the following response headers:
Automatically detected based on file extension. Falls back to application/octet-stream for unknown types.Supported MIME types:
image/png - .png files
image/jpeg - .jpg, .jpeg files
image/gif - .gif files
image/svg+xml - .svg files
application/pdf - .pdf files
text/plain - .txt files
text/html - .html files
application/json - .json files
application/octet-stream - all other files
The size of the file in bytes. Allows browsers to show download progress.
Set to inline; filename="{filename}" which suggests to browsers to display the file (e.g., images, PDFs) rather than downloading it. The filename is included for browser reference.
Behavior Details
File Validation
The function validates that:
- The file exists at the specified path
- The path resolves to an actual file (not a directory)
- The file is within the configured mount directory
If the file doesn’t exist or is not a regular file, the function throws a 404 error.
Streaming vs. Loading
This function uses streaming, which is more efficient than loading the entire file into memory:
- Memory efficient - Files are read in chunks
- Fast start - Client receives data immediately
- Scalable - Can handle large files without consuming excessive memory
Examples
Basic File Retrieval
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
// Stream the file to the client
return await retrieveFileLocally(
event,
filename as string,
'/userFiles'
)
})
Image Endpoint
// server/api/images/[filename].get.ts
export default defineEventHandler(async (event) => {
const filename = getRouterParam(event, 'filename')
if (!filename) {
throw createError({ statusCode: 400, message: 'Filename required' })
}
return await retrieveFileLocally(event, filename, '/images')
})
// Access: GET /api/images/photo.png
With Authentication
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
// Check if user is authenticated
const user = await getUserFromEvent(event)
if (!user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
// Verify user has access to this file
const hasAccess = await checkFileAccess(user.id, filename as string)
if (!hasAccess) {
throw createError({ statusCode: 403, message: 'Forbidden' })
}
return await retrieveFileLocally(event, filename as string, '/private')
})
User Avatar Endpoint
// server/api/users/[userId]/avatar.get.ts
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'userId')
if (!userId) {
throw createError({ statusCode: 400, message: 'User ID required' })
}
// Get user's avatar filename from database
const user = await db.users.findById(userId)
if (!user?.avatarFilename) {
throw createError({ statusCode: 404, message: 'Avatar not found' })
}
return await retrieveFileLocally(
event,
user.avatarFilename,
`/avatars/${userId}`
)
})
// Access: GET /api/users/123/avatar
Download Endpoint with Custom Filename
export default defineEventHandler(async (event) => {
const { filename, downloadName } = getQuery(event)
// Override the Content-Disposition to force download with custom name
const stream = await retrieveFileLocally(
event,
filename as string,
'/documents'
)
// Change to attachment to force download
event.node.res.setHeader(
'Content-Disposition',
`attachment; filename="${downloadName || filename}"`
)
return stream
})
PDF Viewer Endpoint
export default defineEventHandler(async (event) => {
const { documentId } = getQuery(event)
// Get document info from database
const document = await db.documents.findById(documentId as string)
if (!document) {
throw createError({ statusCode: 404, message: 'Document not found' })
}
// Stream PDF file
return await retrieveFileLocally(
event,
document.filename,
'/documents'
)
})
// Embed in HTML: <embed src="/api/document?documentId=123" type="application/pdf" />
Conditional File Serving
export default defineEventHandler(async (event) => {
const { filename, size } = getQuery(event)
let filelocation = '/uploads'
// Serve different versions based on query params
if (size === 'thumbnail') {
filelocation = '/uploads/thumbnails'
} else if (size === 'medium') {
filelocation = '/uploads/medium'
}
return await retrieveFileLocally(
event,
filename as string,
filelocation
)
})
Error Handling with Fallback
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
try {
return await retrieveFileLocally(
event,
filename as string,
'/uploads'
)
} catch (error) {
// Return a default image if file not found
if (error.statusCode === 404) {
return await retrieveFileLocally(
event,
'default-placeholder.png',
'/assets'
)
}
throw error
}
})
Range Request Support (for video streaming)
import { stat, createReadStream } from 'fs'
import { promisify } from 'util'
const statAsync = promisify(stat)
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
const range = getHeader(event, 'range')
const filePath = getFileLocally(filename as string, '/videos')
const stats = await statAsync(filePath)
if (!range) {
// No range request, stream entire file
return await retrieveFileLocally(event, filename as string, '/videos')
}
// Handle range request for video streaming
const parts = range.replace(/bytes=/, '').split('-')
const start = parseInt(parts[0], 10)
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1
const chunksize = (end - start) + 1
event.node.res.statusCode = 206
event.node.res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`)
event.node.res.setHeader('Accept-Ranges', 'bytes')
event.node.res.setHeader('Content-Length', chunksize)
event.node.res.setHeader('Content-Type', 'video/mp4')
return createReadStream(filePath, { start, end })
})
Error Responses
The file doesn’t exist or the path resolves to a directory instead of a file.
The resolved path is outside the configured mount directory (security error).
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
try {
return await retrieveFileLocally(
event,
filename as string,
'/uploads'
)
} catch (error) {
if (error.statusCode === 404) {
throw createError({
statusCode: 404,
message: 'File not found'
})
}
if (error.statusCode === 400) {
throw createError({
statusCode: 400,
message: 'Invalid file path'
})
}
throw error
}
})
For frequently accessed files, consider adding caching headers:
export default defineEventHandler(async (event) => {
const { filename } = getQuery(event)
// Add cache headers for better performance
event.node.res.setHeader('Cache-Control', 'public, max-age=31536000')
event.node.res.setHeader('ETag', generateETag(filename as string))
return await retrieveFileLocally(
event,
filename as string,
'/static-assets'
)
})
Use Cases
- Image serving - Display user-uploaded images
- Document viewing - Serve PDFs and documents for inline viewing
- File downloads - Allow users to download their files
- Avatar endpoints - Serve user profile pictures
- Protected files - Serve files with authentication/authorization
- Media streaming - Stream video and audio files
- API responses - Return files as API responses
See Also
Source
View the source code on GitHub.