diff --git a/apps/docs/content/docs/en/platform/self-hosting/object-storage.mdx b/apps/docs/content/docs/en/platform/self-hosting/object-storage.mdx index 5a15538a0a5..b0dd2d1a03f 100644 --- a/apps/docs/content/docs/en/platform/self-hosting/object-storage.mdx +++ b/apps/docs/content/docs/en/platform/self-hosting/object-storage.mdx @@ -245,6 +245,7 @@ cat > /tmp/cors.json <<'EOF' "x-goog-meta-purpose", "x-goog-meta-userid", "x-goog-meta-workspaceid", + "x-goog-meta-folderid", "x-goog-meta-workflowid", "x-goog-meta-executionid" ], diff --git a/apps/sim/lib/uploads/client/direct-upload.test.ts b/apps/sim/lib/uploads/client/direct-upload.test.ts index 26c8bb99a1e..da2b2aca65f 100644 --- a/apps/sim/lib/uploads/client/direct-upload.test.ts +++ b/apps/sim/lib/uploads/client/direct-upload.test.ts @@ -82,6 +82,47 @@ describe('runUploadStrategy', () => { expect(MockXHR.instances[0].open).toHaveBeenCalledWith('PUT', 'https://s3/presigned') }) + it('sets Content-Type exactly once when uploadHeaders already carry it (GCS signed uploads)', async () => { + const file = makeFile(1024) + + await runUploadStrategy({ + file, + workspaceId: 'ws-1', + context: 'workspace', + presignedOverride: presigned({ + uploadHeaders: { + 'Content-Type': 'application/octet-stream', + 'x-goog-meta-workspaceid': 'ws-1', + }, + }), + }) + + const calls = MockXHR.instances[0].setRequestHeader.mock.calls + const contentTypeCalls = calls.filter( + ([k]: [string, string]) => k.toLowerCase() === 'content-type' + ) + expect(contentTypeCalls).toHaveLength(1) + expect(contentTypeCalls[0][1]).toBe('application/octet-stream') + expect(calls.some(([k]: [string, string]) => k === 'x-goog-meta-workspaceid')).toBe(true) + }) + + it('falls back to the file content type when uploadHeaders omit Content-Type', async () => { + const file = makeFile(1024) + + await runUploadStrategy({ + file, + workspaceId: 'ws-1', + context: 'workspace', + presignedOverride: presigned({ uploadHeaders: { 'x-ms-blob-type': 'BlockBlob' } }), + }) + + const calls = MockXHR.instances[0].setRequestHeader.mock.calls + const contentTypeCalls = calls.filter( + ([k]: [string, string]) => k.toLowerCase() === 'content-type' + ) + expect(contentTypeCalls).toHaveLength(1) + }) + it('throws FALLBACK_REQUIRED when server signals no cloud storage', async () => { const file = makeFile(ONE_MB) diff --git a/apps/sim/lib/uploads/client/direct-upload.ts b/apps/sim/lib/uploads/client/direct-upload.ts index ad448f891e3..41e9911c26d 100644 --- a/apps/sim/lib/uploads/client/direct-upload.ts +++ b/apps/sim/lib/uploads/client/direct-upload.ts @@ -292,7 +292,12 @@ const uploadViaPresignedPut = (opts: UploadViaPutOptions): Promise => { }) xhr.open('PUT', presignedUrl) - xhr.setRequestHeader('Content-Type', getFileContentType(file)) + const providesContentType = + uploadHeaders && + Object.keys(uploadHeaders).some((key) => key.toLowerCase() === 'content-type') + if (!providesContentType) { + xhr.setRequestHeader('Content-Type', getFileContentType(file)) + } if (uploadHeaders) { for (const [key, value] of Object.entries(uploadHeaders)) { xhr.setRequestHeader(key, value)