Skip to content

Commit 9ca70ed

Browse files
committed
Omit Content-Length header when FormData has entry with unknown length
1 parent f1c85bd commit 9ca70ed

File tree

3 files changed

+48
-19
lines changed

3 files changed

+48
-19
lines changed

src/FormDataEncoder.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,25 @@ test("Content-Length header is read-only", t => {
188188
t.is(headers["Content-Length"], expected)
189189
})
190190

191+
test(
192+
"Does not return Content-Length header "
193+
+ "if FormData has entry without known length",
194+
195+
t => {
196+
const form = new FormData()
197+
198+
form.set("stream", {
199+
[Symbol.toStringTag]: "File",
200+
name: "file.txt",
201+
stream() { }
202+
})
203+
204+
const encoder = new FormDataEncoder(form)
205+
206+
t.false("Content-Length" in encoder.headers)
207+
}
208+
)
209+
191210
test("Yields correct footer for empty FormData", async t => {
192211
const encoder = new FormDataEncoder(new FormData())
193212

@@ -208,7 +227,7 @@ test("Returns correct length of the empty FormData content", async t => {
208227
const encoder = new FormDataEncoder(new FormData())
209228
const expected = await readStream(encoder).then(({length}) => length)
210229

211-
t.is<number, number>(encoder.getContentLength(), expected)
230+
t.is(encoder.getContentLength(), expected)
212231
})
213232

214233
test("Returns the length of the FormData content", async t => {
@@ -221,7 +240,7 @@ test("Returns the length of the FormData content", async t => {
221240

222241
const expected = await readStream(encoder).then(({length}) => length)
223242

224-
t.is<number, number>(encoder.getContentLength(), expected)
243+
t.is(encoder.getContentLength(), expected)
225244
})
226245

227246
test(".values() yields headers as Uint8Array", t => {

src/FormDataEncoder.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {isFile} from "./util/isFile.js"
1111

1212
type FormDataEntryValue = string | FileLike
1313

14-
type RawHeaders = Readonly<{
14+
interface RawHeaders {
1515
"Content-Type": string
16-
"Content-Length": string
17-
}>
16+
"Content-Length"?: string
17+
}
1818

1919
export type FormDataEncoderHeaders =
20-
& RawHeaders
21-
& LowercaseObjectKeys<RawHeaders>
20+
& Readonly<RawHeaders>
21+
& Readonly<LowercaseObjectKeys<RawHeaders>>
2222

2323
export interface FormDataEncoderOptions {
2424
/**
@@ -83,7 +83,7 @@ export class FormDataEncoder {
8383
/**
8484
* Returns Content-Length header
8585
*/
86-
readonly contentLength: string
86+
readonly contentLength: string | undefined
8787

8888
/**
8989
* Returns headers object with Content-Type and Content-Length header
@@ -174,12 +174,17 @@ export class FormDataEncoder {
174174
`${this.#DASHES}${this.boundary}${this.#DASHES}${this.#CRLF.repeat(2)}`
175175
)
176176

177-
this.contentLength = this.#getContentLength()
177+
const headers: RawHeaders = {
178+
"Content-Type": this.contentType
179+
}
180+
181+
const contentLength = this.#getContentLength()
182+
if (contentLength) {
183+
this.contentLength = contentLength
184+
headers["Content-Length"] = contentLength
185+
}
178186

179-
this.headers = proxyHeaders(Object.freeze({
180-
"Content-Type": this.contentType,
181-
"Content-Length": this.contentLength
182-
}))
187+
this.headers = proxyHeaders(Object.freeze(headers))
183188

184189
// Make sure following properties read-only in runtime.
185190
Object.defineProperties(this, {
@@ -214,17 +219,24 @@ export class FormDataEncoder {
214219
/**
215220
* Returns form-data content length
216221
*/
217-
#getContentLength(): string {
222+
#getContentLength(): string | undefined {
218223
let length = 0
219224

220225
for (const [name, raw] of this.#form) {
221226
const value = isFile(raw) ? raw : this.#encoder.encode(
222227
normalizeValue(raw)
223228
)
224229

230+
const size = isFile(value) ? value.size : value.byteLength
231+
232+
// Return `undefined` if encountered part without known size
233+
if (size == null || Number.isNaN(size)) {
234+
return undefined
235+
}
236+
225237
length += this.#getFieldHeader(name, value).byteLength
226238

227-
length += isFile(value) ? value.size : value.byteLength
239+
length += size
228240

229241
length += this.#CRLF_BYTES_LENGTH
230242
}
@@ -237,8 +249,8 @@ export class FormDataEncoder {
237249
*
238250
* @deprecated Use FormDataEncoder.contentLength or FormDataEncoder.headers["Content-Length"] instead
239251
*/
240-
getContentLength(): number {
241-
return Number(this.contentLength)
252+
getContentLength(): number | undefined {
253+
return this.contentLength == null ? undefined : Number(this.contentLength)
242254
}
243255

244256
/**

src/util/isFile.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ export const isFile = (value: unknown): value is FileLike => Boolean(
3636
&& (value as FileLike)[Symbol.toStringTag] === "File"
3737
&& isFunction((value as FileLike).stream)
3838
&& (value as FileLike).name != null
39-
&& (value as FileLike).size != null
40-
&& (value as FileLike).lastModified != null
4139
)
4240

4341
/**

0 commit comments

Comments
 (0)