Safari neescapuje filename v multipart — undici formData() hází „Failed to parse body as FormData"
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Upload obrázků intermitentně padá na 500 s TypeError: Failed to parse body as FormData. — ale JEN u některých uživatelů a jen u některých souborů. V access logu 100% korelace s jedním prohlížečem: všechna selhání Safari, Chrome tentýž den na stejném endpointu prochází. Tentýž Safari uživatel jindy uspěje.
Root cause (potvrzená reprodukce; produkční příčina vysoce pravděpodobná)
Sekce “Root cause (potvrzená reprodukce; produkční příčina vysoce pravděpodobná)”Produkční logy původní filename neobsahují — důkaz tvoří 100% korelace selhání s jedním prohlížečem + lokální reprodukce IDENTICKÉ chyby. Definitivní potvrzení vyžaduje název souboru od postiženého uživatele (bezpečná reprodukce bez PII).
Mechanismus
Sekce “Mechanismus”WebKit/Safari posílá Content-Disposition: form-data; name="file"; filename="…" s názvem souboru RAW, neescapovaným — Chrome/Firefox " escapují na %22. Undici (Node fetch API) part-header striktně validuje a při neescapované uvozovce, \n nebo \r ve filename vyhodí přesně tuto chybu. Soubor Foto "Krajina" finál.jpg tak v Safari shodí celý parse, IMG_1234.jpg projde — odtud zdánlivá náhodnost.
Reprodukce (Node, bez prohlížeče):
const b = "----boundary";const body = [`--${b}`, `Content-Disposition: form-data; name="file"; filename="foto\"test.png"`, "Content-Type: image/png", "", "DATA", `--${b}--`, ""].join("\r\n");await new Request("http://x/", { method: "POST", body, headers: { "content-type": `multipart/form-data; boundary=${b}` } }).formData();// → TypeError: Failed to parse body as FormData.Fix
Sekce “Fix”Sdílený wrapper: try { await request.formData() } catch → fallback parse těla přes arrayBuffer() + tolerantní parser (busboy), nebo minimálně 400 s hláškou „přejmenujte soubor”. Filename vždy sanitizovat server-side. Diagnostický klíč: v access logu porovnat User-Agent úspěchů vs. selhání — browser-exkluzivní chybovost ukazuje na rozdíl v konstrukci requestu, ne na server.