Skip to content

Commit 57fc40b

Browse files
committed
Proxy encoder headers so that can be accessed with case-insensitive manner.
1 parent 1b62adb commit 57fc40b

File tree

4 files changed

+99
-7
lines changed

4 files changed

+99
-7
lines changed

src/FormDataEncoder.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,63 @@ test("contentLength property cannot be deleted", t => {
131131
test("Has correct headers", async t => {
132132
const encoder = new FormDataEncoder(new FormData())
133133

134-
t.deepEqual(encoder.headers, {
134+
t.deepEqual({...encoder.headers}, {
135135
"Content-Type": `multipart/form-data; boundary=${encoder.boundary}`,
136136
"Content-Length": await readStream(encoder).then(({length}) => `${length}`)
137137
})
138138
})
139139

140+
test("Headers can be accessed by lowercased keys", async t => {
141+
const encoder = new FormDataEncoder(new FormData())
142+
143+
t.true("content-type" in encoder.headers)
144+
t.is(
145+
encoder.headers["content-type"],
146+
147+
`multipart/form-data; boundary=${encoder.boundary}`
148+
)
149+
150+
t.true("content-length" in encoder.headers)
151+
t.is(
152+
encoder.headers["content-length"],
153+
154+
await readStream(encoder).then(({length}) => `${length}`)
155+
)
156+
})
157+
158+
test("FormDataEncoder.headers property is read-only", t => {
159+
const encoder = new FormDataEncoder(new FormData())
160+
161+
const expected = {...encoder.headers}
162+
163+
// @ts-expect-error
164+
try { encoder.headers = {foo: "foo"} } catch { /* noop */ }
165+
166+
t.deepEqual({...encoder.headers}, expected)
167+
})
168+
169+
test("Content-Type header is read-only", t => {
170+
const {headers} = new FormDataEncoder(new FormData())
171+
172+
const expected = headers["Content-Type"]
173+
174+
// @ts-expect-error
175+
try { headers["Content-Type"] = "can't override" } catch { /* noop */ }
176+
177+
t.is(headers["Content-Type"], expected)
178+
})
179+
180+
test("Content-Length header is read-only", t => {
181+
const {headers} = new FormDataEncoder(new FormData())
182+
183+
const expected = headers["Content-Length"]
184+
185+
// @ts-expect-error
186+
try { headers["Content-Length"] = "can't override" } catch { /* noop */ }
187+
188+
t.is(headers["Content-Length"], expected)
189+
})
190+
140191
test("Yields correct footer for empty FormData", async t => {
141192
const encoder = new FormDataEncoder(new FormData())
142193

src/FormDataEncoder.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@ import escape from "./util/escapeName.js"
55

66
import {isFile} from "./util/isFile.js"
77
import {isFormData} from "./util/isFormData.js"
8+
import {proxyHeaders} from "./util/proxyHeaders.js"
9+
import type {LowercaseObjectKeys} from "./util/LowercaseObjectKeys.js"
810
import {FormDataLike} from "./FormDataLike.js"
911
import {FileLike} from "./FileLike.js"
1012

1113
type FormDataEntryValue = string | FileLike
1214

15+
type RawHeaders = Readonly<{
16+
"Content-Type": string
17+
"Content-Length": string
18+
}>
19+
20+
export type FormDataEncoderHeaders =
21+
& RawHeaders
22+
& LowercaseObjectKeys<RawHeaders>
23+
1324
export interface FormDataEncoderOptions {
1425
/**
1526
* When enabled, the encoder will emit additional per part headers, such as `Content-Length`.
@@ -76,10 +87,7 @@ export class FormDataEncoder {
7687
/**
7788
* Returns headers object with Content-Type and Content-Length header
7889
*/
79-
readonly headers: {
80-
"Content-Type": string
81-
"Content-Length": string
82-
}
90+
readonly headers: FormDataEncoderHeaders
8391

8492
/**
8593
* Creates a multipart/form-data encoder.
@@ -167,10 +175,10 @@ export class FormDataEncoder {
167175

168176
this.contentLength = String(this.#getContentLength())
169177

170-
this.headers = Object.freeze({
178+
this.headers = proxyHeaders(Object.freeze({
171179
"Content-Type": this.contentType,
172180
"Content-Length": this.contentLength
173-
})
181+
}))
174182

175183
// Make sure following properties read-only in runtime.
176184
Object.defineProperties(this, {

src/util/LowercaseObjectKeys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Baed on: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5336057c43fcd14eabe7ae8536b51a7c7b2b21bf/types/lowercase-object-keys/index.d.ts
2+
export type LowercaseObjectKeys<T extends object> = {
3+
[K in keyof T as K extends string ? Lowercase<K> : K]: T[K]
4+
}

src/util/proxyHeaders.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type {LowercaseObjectKeys} from "./LowercaseObjectKeys.js"
2+
3+
type AnyObject = Record<string | symbol, string>
4+
5+
function getProperty<T extends AnyObject>(
6+
target: T, prop: string | symbol
7+
): string | undefined {
8+
if (typeof prop !== "string") {
9+
return target[prop]
10+
}
11+
12+
for (const [name, value] of Object.entries(target)) {
13+
if (prop.toLowerCase() === name.toLowerCase()) {
14+
return value
15+
}
16+
}
17+
18+
return undefined
19+
}
20+
21+
export const proxyHeaders = <T extends AnyObject>(target: T) => new Proxy(
22+
target,
23+
24+
{
25+
get: (target, prop) => getProperty(target, prop),
26+
27+
has: (target, prop) => getProperty(target, prop) !== undefined
28+
}
29+
) as T & LowercaseObjectKeys<T>

0 commit comments

Comments
 (0)