Skip to content

Commit 48aeac2

Browse files
authored
fix(uploads): set Content-Type once on presigned PUTs; document x-goog-meta-folderid for GCS CORS (#6121)
* fix(uploads): set Content-Type once on presigned PUTs; document x-goog-meta-folderid in the GCS CORS example XMLHttpRequest.setRequestHeader appends on repeated calls (values join with a comma), and GCS is the only provider whose signed uploadHeaders include Content-Type — so single-shot GCS uploads sent 'x, x', which fails V4 signature verification with 403 (headers canonicalize to a comma-separated value that must match what was signed; multipart part PUTs are unaffected since part URLs don't sign Content-Type). The client now sets its default Content-Type only when the server's signed headers don't already carry one, with regression tests for both paths. Also adds x-goog-meta-folderid to the documented GCS CORS responseHeader list — workspace uploads now sign a folderId metadata header, and GCS CORS matches preflight request headers against that list exactly (no wildcards), so the missing entry blocked browser uploads into folders. * chore(uploads): drop inline comment
1 parent 413784e commit 48aeac2

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

apps/docs/content/docs/en/platform/self-hosting/object-storage.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ cat > /tmp/cors.json <<'EOF'
245245
"x-goog-meta-purpose",
246246
"x-goog-meta-userid",
247247
"x-goog-meta-workspaceid",
248+
"x-goog-meta-folderid",
248249
"x-goog-meta-workflowid",
249250
"x-goog-meta-executionid"
250251
],

apps/sim/lib/uploads/client/direct-upload.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ describe('runUploadStrategy', () => {
8282
expect(MockXHR.instances[0].open).toHaveBeenCalledWith('PUT', 'https://s3/presigned')
8383
})
8484

85+
it('sets Content-Type exactly once when uploadHeaders already carry it (GCS signed uploads)', async () => {
86+
const file = makeFile(1024)
87+
88+
await runUploadStrategy({
89+
file,
90+
workspaceId: 'ws-1',
91+
context: 'workspace',
92+
presignedOverride: presigned({
93+
uploadHeaders: {
94+
'Content-Type': 'application/octet-stream',
95+
'x-goog-meta-workspaceid': 'ws-1',
96+
},
97+
}),
98+
})
99+
100+
const calls = MockXHR.instances[0].setRequestHeader.mock.calls
101+
const contentTypeCalls = calls.filter(
102+
([k]: [string, string]) => k.toLowerCase() === 'content-type'
103+
)
104+
expect(contentTypeCalls).toHaveLength(1)
105+
expect(contentTypeCalls[0][1]).toBe('application/octet-stream')
106+
expect(calls.some(([k]: [string, string]) => k === 'x-goog-meta-workspaceid')).toBe(true)
107+
})
108+
109+
it('falls back to the file content type when uploadHeaders omit Content-Type', async () => {
110+
const file = makeFile(1024)
111+
112+
await runUploadStrategy({
113+
file,
114+
workspaceId: 'ws-1',
115+
context: 'workspace',
116+
presignedOverride: presigned({ uploadHeaders: { 'x-ms-blob-type': 'BlockBlob' } }),
117+
})
118+
119+
const calls = MockXHR.instances[0].setRequestHeader.mock.calls
120+
const contentTypeCalls = calls.filter(
121+
([k]: [string, string]) => k.toLowerCase() === 'content-type'
122+
)
123+
expect(contentTypeCalls).toHaveLength(1)
124+
})
125+
85126
it('throws FALLBACK_REQUIRED when server signals no cloud storage', async () => {
86127
const file = makeFile(ONE_MB)
87128

apps/sim/lib/uploads/client/direct-upload.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,12 @@ const uploadViaPresignedPut = (opts: UploadViaPutOptions): Promise<void> => {
292292
})
293293

294294
xhr.open('PUT', presignedUrl)
295-
xhr.setRequestHeader('Content-Type', getFileContentType(file))
295+
const providesContentType =
296+
uploadHeaders &&
297+
Object.keys(uploadHeaders).some((key) => key.toLowerCase() === 'content-type')
298+
if (!providesContentType) {
299+
xhr.setRequestHeader('Content-Type', getFileContentType(file))
300+
}
296301
if (uploadHeaders) {
297302
for (const [key, value] of Object.entries(uploadHeaders)) {
298303
xhr.setRequestHeader(key, value)

0 commit comments

Comments
 (0)