mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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
This commit is contained in:
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -292,7 +292,12 @@ const uploadViaPresignedPut = (opts: UploadViaPutOptions): Promise<void> => {
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user