How to Fix: German umlauts in file names are encoded when uploading via server action form data
German umlauts like ä, ö, ü, and ß are not actually being corrupted during upload; they are being exposed in their MIME header encoded form when a file is submitted through FormData to a Next.js Server Action. That is why a filename such as Prüfung.pdf can appear as something like =?UTF-8?Q?Pr=C3=BCfung.pdf?= instead of the human-readable original name.
Understanding the Root Cause
This bug sits at the intersection of multipart/form-data parsing, HTTP header encoding, and the way Next.js Server Actions currently expose uploaded files.
When a browser uploads a file, the filename is typically sent inside the Content-Disposition header of a multipart request. Headers are historically ASCII-oriented, so non-ASCII characters such as German umlauts may be represented using an encoded-word format or another transport-safe encoding rather than plain Unicode text.
In a normal upload pipeline, the server-side parser should decode that transport encoding before your application reads file.name. In this issue, the filename reaches the Server Action in an already encoded representation, which means the decoding step is either missing or not happening in the expected layer.
So the root problem is not the file system, not the browser, and usually not your form markup. The real issue is that the multipart filename metadata is surfaced without proper Unicode normalization/decoding before you use it in application code.
This explains why:
- The uploaded file content itself is fine.
- Only the filename looks wrong.
- The problem is easiest to reproduce with ä, ö, ü, ß and other non-ASCII characters.
- The behavior appears specifically in a server action + form data upload flow.
Step-by-Step Solution
The safest application-level workaround is to decode MIME-encoded filenames manually before storing or displaying them.
If your Server Action receives a filename such as =?UTF-8?Q?Pr=C3=BCfung.pdf?=, you can detect and decode it yourself.
1. Read the uploaded file in the Server Action
'use server'
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File | null
if (!file) {
throw new Error('No file uploaded')
}
const originalName = file.name
console.log('Raw filename:', originalName)
}
2. Add a MIME filename decoder
Many affected filenames arrive in RFC 2047 Q-encoding form. This helper handles the common UTF-8 case seen in this bug.
function decodeMimeEncodedFilename(filename: string): string {
const match = filename.match(/^=\?UTF-8\?Q\?(.+)\?=$/i)
if (!match) {
return filename
}
const encoded = match[1]
.replace(/_/g, ' ')
.replace(/=([A-F0-9]{2})/gi, '%$1')
try {
return decodeURIComponent(encoded)
} catch {
return filename
}
}
3. Normalize the filename before saving
'use server'
import { writeFile } from 'node:fs/promises'
import path from 'node:path'
function decodeMimeEncodedFilename(filename: string): string {
const match = filename.match(/^=\?UTF-8\?Q\?(.+)\?=$/i)
if (!match) {
return filename
}
const encoded = match[1]
.replace(/_/g, ' ')
.replace(/=([A-F0-9]{2})/gi, '%$1')
try {
return decodeURIComponent(encoded)
} catch {
return filename
}
}
function sanitizeFilename(filename: string): string {
return filename
.replace(/[\\/<>:"|?*\x00-\x1F]/g, '_')
.trim()
}
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File | null
if (!file) {
throw new Error('No file uploaded')
}
const decodedName = decodeMimeEncodedFilename(file.name)
const safeName = sanitizeFilename(decodedName).normalize('NFC')
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filePath = path.join(process.cwd(), 'uploads', safeName)
await writeFile(filePath, buffer)
return {
original: file.name,
decoded: decodedName,
savedAs: safeName
}
}
4. Use the decoded name everywhere user-facing
If you show upload results in the UI, always render the decoded filename rather than the raw file.name value.
const result = await uploadFile(formData)
console.log(result.decoded)
5. Prefer a stable storage strategy
In production systems, it is often better to store files under a generated ID and save the decoded original filename separately in a database. That avoids collisions and prevents filename parsing bugs from affecting storage paths.
const storedFileName = crypto.randomUUID()
const originalDisplayName = sanitizeFilename(decodedName).normalize('NFC')
6. Track framework fixes upstream
Because this is rooted in framework/runtime multipart handling, also monitor the upstream issue and related discussions in the reproduction repository and framework tracker. If Next.js or the underlying parser begins decoding filenames correctly, you may be able to remove the workaround later.
Reference reproduction: demo repository.
Common Edge Cases
- Different encodings: Not every malformed filename will use the exact =?UTF-8?Q?…?= pattern. Some clients may send other encodings or formats.
- Mixed Unicode normalization: Even after decoding, filenames can differ between NFC and NFD. Normalize before comparing or storing names.
- Unsafe path characters: A decoded filename may include slashes, control characters, or reserved symbols. Always sanitize before writing to disk.
- Name collisions: Two users can upload files with the same decoded name. Use unique storage keys instead of raw filenames as disk names.
- Cross-platform behavior: A filename that works on Linux may fail on Windows because of reserved characters or device names.
- Very long filenames: Decoded Unicode names can exceed file system or database limits. Truncate safely if needed.
- Display vs storage mismatch: If you save the raw encoded name but display the decoded version, later downloads may look inconsistent.
FAQ
Why does this only happen with German umlauts or other special characters?
ASCII characters survive multipart headers unchanged, but characters like ü require encoding in header metadata. If that encoding is not decoded by the server layer, you see the transport representation instead of the original text.
Is this a browser bug or a Next.js bug?
In this scenario, the browser is usually sending a valid multipart request. The problem is more likely in the multipart parsing/runtime layer that feeds data into the Server Action, where the filename is exposed without proper decoding.
Should I rename files on the client before upload?
You can, but it is usually not the best fix. Client-side renaming changes the user’s original filename and can create product or compliance issues. A better approach is to decode, sanitize, normalize, and store safely on the server.
The practical fix is straightforward: treat file.name from a Server Action upload as potentially transport-encoded, decode it explicitly, normalize it, sanitize it, and separate storage filename from display filename. That resolves the umlaut problem today while staying compatible with a future framework-level fix.