Skip to content

generateContentDisposition fails with non-ASCII filenames (ERR_INVALID_CHAR) #1498

@gtolarc

Description

@gtolarc

Environment

  • @orpc/server: 1.13.8
  • @orpc/standard-server: 1.13.10
  • Node.js: v22.17.0
  • Fastify: 5.8.4

Reproduction

import { os } from '@orpc/server'
import { readFile } from 'fs/promises'
import * as z from 'zod'

const downloadTemplate = os
  .route({ method: 'GET', path: '/template' })
  .output(z.any())
  .handler(async () => {
    const buffer = await readFile('./テンプレート.xlsx')
    return new File([buffer], 'テンプレート.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    })
  })

Describe the bug

Description

generateContentDisposition in @orpc/standard-server produces an invalid Content-Disposition header when the File.name contains non-ASCII characters (e.g. Korean, Japanese, Chinese). Node.js ServerResponse.setHeader rejects the header with
ERR_INVALID_CHAR.

Current behavior

// @orpc/standard-server/dist/index.mjs
function generateContentDisposition(filename) {
  const escapedFileName = filename.replace(/"/g, '\\"');
  const encodedFilenameStar = encodeURIComponent(filename)...;
  return `inline; filename="${escapedFileName}"; filename*=utf-8''${encodedFilenameStar}`;
}

The filename*=utf-8''... part is correctly percent-encoded, but filename="${escapedFileName}" keeps non-ASCII characters as-is. This causes:

TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["content-disposition"]
    at ServerResponse.setHeader (node:_http_outgoing:703:3)

Expected behavior

Per RFC 6266, the filename= parameter should be an ASCII fallback, while filename*= carries the UTF-8 encoded name:

Content-Disposition: inline; filename="template.xlsx"; filename*=utf-8''%ED%85%9C%ED%94%8C%EB%A6%BF.xlsx

Suggested fix

Strip non-ASCII from filename= or use a generic ASCII fallback:

function generateContentDisposition(filename) {
  const asciiFilename = filename.replace(/[^\x20-\x7E]/g, '_');
  const encodedFilenameStar = encodeURIComponent(filename)
    .replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
    .replace(/%(7C|60|5E)/g, (str, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
  return `inline; filename="${asciiFilename}"; filename*=utf-8''${encodedFilenameStar}`;
}

Additional context

No response

Logs

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions