Plane's frontend calls getFileMetaDataForUpload() which uses the file-type library to sniff MIME from magic bytes. For unsniffable formats (plain text, .json, .csv, etc.) it returns "" — and that empty string was being threaded through to S3Storage.generate_presigned_post(), signing the presigned URL with `Content-Type=""`. Browsers can't reliably send an empty Content-Type header, so the SigV4 signature never matched and R2 returned 403 SignatureDoesNotMatch. UI showed an opaque upload error. Two-sided fix: * apps/api/plane/settings/storage.py — default file_type to "application/octet-stream" when empty/None. The signed URL now always has a non-empty Content-Type the browser can match. * packages/services/src/file/helper.ts — generateFileUploadPayload now prefers the signed Content-Type from upload_data.fields["Content-Type"] over file.type. The browser must send EXACTLY the signed value, not its own MIME guess from extension. Belt-and-suspenders defense alongside the backend default. Reproduced empirically against R2 with the new keys 2026-05-01: empty Content-Type signs, then PUT with `Content-Type: text/plain` returns 403 SignatureDoesNotMatch. With this patch, signing "application/octet-stream" + sending it back verbatim returns 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| .prettierignore | ||
| package.json | ||
| tsconfig.json | ||
| tsdown.config.ts | ||