Skip to content

Commit 5dd3c83

Browse files
committed
fix(uploads): leave an already-consumed multipart body as a server error
1 parent aac5262 commit 5dd3c83

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

‎apps/sim/lib/core/utils/stream-limits.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,30 @@ describe('stream limits', () => {
319319
).rejects.toBeInstanceOf(MultipartFieldValidationError)
320320
})
321321

322+
it('leaves a body that was already read to fail as a server error', async () => {
323+
const input = new FormData()
324+
input.append('name', 'example')
325+
const encoded = new Request('http://localhost/upload', { method: 'POST', body: input })
326+
const bytes = new Uint8Array(await encoded.arrayBuffer())
327+
const request = new Request('http://localhost/upload', {
328+
method: 'POST',
329+
headers: {
330+
'content-type': encoded.headers.get('content-type') ?? '',
331+
'content-length': String(bytes.byteLength),
332+
},
333+
body: bytes,
334+
})
335+
await request.text()
336+
337+
const failure = await readFormDataWithLimit(request, {
338+
maxBytes: 1024 * 1024,
339+
label: 'multipart body',
340+
}).catch((error: unknown) => error)
341+
342+
expect(failure).toBeInstanceOf(TypeError)
343+
expect(failure).not.toBeInstanceOf(MultipartFieldValidationError)
344+
})
345+
322346
it('rejects multipart streams without content-length once bytes exceed the limit', async () => {
323347
const request = new Request('http://localhost/upload', {
324348
method: 'POST',

‎apps/sim/lib/core/utils/stream-limits.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface ReadFormDataWithLimitRequest {
6969
method: string
7070
headers?: Headers
7171
body?: ReadableStream<Uint8Array> | null
72+
bodyUsed?: boolean
7273
formData: () => Promise<FormData>
7374
}
7475

@@ -158,9 +159,14 @@ export async function readFormDataWithLimit(
158159
* The fetch body parser rejects a body that does not decode as the content type it
159160
* declares (a JSON body, a boundary the body never uses) with a bare `TypeError`. That
160161
* is the caller's malformed input, so it surfaces as the same validation failure every
161-
* multipart route already answers with a 400.
162+
* multipart route already answers with a 400. So does a declared-length body that ends
163+
* early, since the client sent an incomplete request. A body some earlier code already
164+
* consumed is a server bug, so that failure is left to surface as it is.
162165
*/
163-
async function parseFormData(request: Pick<Request, 'formData'>): Promise<FormData> {
166+
async function parseFormData(
167+
request: Pick<ReadFormDataWithLimitRequest, 'formData' | 'bodyUsed'>
168+
): Promise<FormData> {
169+
if (request.bodyUsed) return request.formData()
164170
try {
165171
return await request.formData()
166172
} catch (error) {

0 commit comments

Comments
 (0)