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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions