Skip to content

Commit 4e72f0b

Browse files
committed
Introduce FormDataEncoder.contentLength property. Make sure readonly properties actually read-only in runtime.
1 parent 5af1cfe commit 4e72f0b

File tree

2 files changed

+125
-28
lines changed

2 files changed

+125
-28
lines changed

lib/Encoder.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,30 @@ import {FormDataEncoder} from "./Encoder"
1919
test("Has boundary string", t => {
2020
const encoder = new FormDataEncoder(new FormData())
2121

22-
t.true(typeof encoder.boundary === "string")
22+
t.true("boundary" in encoder)
23+
t.is(typeof encoder.boundary, "string")
24+
})
25+
26+
test("boundary property is read-only", t => {
27+
const encoder = new FormDataEncoder(new FormData())
28+
29+
const {boundary: expected} = encoder
30+
31+
// @ts-expect-error
32+
try { encoder.boundary = "some string" } catch { /* noop */ }
33+
34+
t.is(encoder.boundary, expected)
35+
})
36+
37+
test("boundary property cannot be deleted", t => {
38+
const encoder = new FormDataEncoder(new FormData())
39+
40+
const {boundary: expected} = encoder
41+
42+
// @ts-expect-error
43+
try { delete encoder.boundary } catch { /* noop */ }
44+
45+
t.is(encoder.boundary, expected)
2346
})
2447

2548
test("Accepts custom boundary as the second argument", t => {
@@ -33,9 +56,33 @@ test("Accepts custom boundary as the second argument", t => {
3356
test("Has content-type string", t => {
3457
const encoder = new FormDataEncoder(new FormData())
3558

59+
t.true("contentType" in encoder)
60+
t.is(typeof encoder.contentType, "string")
3661
t.true(encoder.contentType.startsWith("multipart/form-data; boundary="))
3762
})
3863

64+
test("contentType property is read-only", t => {
65+
const encoder = new FormDataEncoder(new FormData())
66+
67+
const {contentType: expected} = encoder
68+
69+
// @ts-expect-error
70+
try { encoder.contentType = "application/json" } catch { /* noop */ }
71+
72+
t.is(encoder.contentType, expected)
73+
})
74+
75+
test("contentType cannot be deleted", t => {
76+
const encoder = new FormDataEncoder(new FormData())
77+
78+
const {contentType: expected} = encoder
79+
80+
// @ts-expect-error
81+
try { delete encoder.contentType } catch { /* noop */ }
82+
83+
t.is(encoder.contentType, expected)
84+
})
85+
3986
test("Has content-type string with custom boundary string", t => {
4087
const expected = "BoundaryString123"
4188

@@ -47,6 +94,40 @@ test("Has content-type string with custom boundary string", t => {
4794
)
4895
})
4996

97+
test("Has contentLength property", async t => {
98+
const encoder = new FormDataEncoder(new FormData())
99+
100+
t.true("contentLength" in encoder)
101+
t.is(typeof encoder.contentLength, "string")
102+
t.is(
103+
encoder.contentLength,
104+
105+
await readStream(encoder).then(({length}) => `${length}`)
106+
)
107+
})
108+
109+
test("contentLength property is read-only", t => {
110+
const encoder = new FormDataEncoder(new FormData())
111+
112+
const {contentLength: expected} = encoder
113+
114+
// @ts-expect-error
115+
try { encoder.contentLength = String(Date.now()) } catch { /* noop */ }
116+
117+
t.is(encoder.contentLength, expected)
118+
})
119+
120+
test("contentLength property cannot be deleted", t => {
121+
const encoder = new FormDataEncoder(new FormData())
122+
123+
const {contentLength: expected} = encoder
124+
125+
// @ts-expect-error
126+
try { encoder.contentLength = String(Date.now()) } catch { /* noop */ }
127+
128+
t.is(encoder.contentLength, expected)
129+
})
130+
50131
test("Has correct headers", async t => {
51132
const encoder = new FormDataEncoder(new FormData())
52133

lib/Encoder.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,7 @@ import {FormDataLike} from "./FormDataLike"
88
import {FileLike} from "./FileLike"
99

1010
export class FormDataEncoder {
11-
/**
12-
* Returns boundary string
13-
*/
14-
readonly boundary: string
15-
16-
/**
17-
* Returns Content-Type header for multipart/form-data
18-
*/
19-
readonly contentType: string
20-
21-
/**
22-
* Returns headers object with Content-Type and Content-Length header
23-
*/
24-
readonly headers: {
25-
"Content-Type": string
26-
"Content-Length": string
27-
}
28-
29-
readonly #CRLF: string
11+
readonly #CRLF: string = "\r\n"
3012

3113
readonly #CRLF_BYTES: Uint8Array
3214

@@ -37,7 +19,7 @@ export class FormDataEncoder {
3719
/**
3820
* TextEncoder instance
3921
*/
40-
readonly #encoder: TextEncoder
22+
readonly #encoder = new TextEncoder()
4123

4224
/**
4325
* Returns form-data footer bytes
@@ -49,6 +31,29 @@ export class FormDataEncoder {
4931
*/
5032
readonly #form: FormDataLike
5133

34+
/**
35+
* Returns boundary string
36+
*/
37+
readonly boundary: string
38+
39+
/**
40+
* Returns Content-Type header
41+
*/
42+
readonly contentType: string
43+
44+
/**
45+
* Returns Content-Length header
46+
*/
47+
readonly contentLength: string
48+
49+
/**
50+
* Returns headers object with Content-Type and Content-Length header
51+
*/
52+
readonly headers: {
53+
"Content-Type": string
54+
"Content-Length": string
55+
}
56+
5257
/**
5358
* Creates a multipart/form-data encoder.
5459
*
@@ -75,23 +80,34 @@ export class FormDataEncoder {
7580
throw new TypeError("Expected boundary to be a string.")
7681
}
7782

78-
this.boundary = `form-data-boundary-${boundary}`
79-
this.contentType = `multipart/form-data; boundary=${this.boundary}`
80-
81-
this.#encoder = new TextEncoder()
83+
// ? Should I preserve FormData entries in array instead?
84+
// ? That way it will be immutable, but require to allocate a new array
85+
// ? and go through entries during initialization.
86+
this.#form = form
8287

83-
this.#CRLF = "\r\n"
8488
this.#CRLF_BYTES = this.#encoder.encode(this.#CRLF)
8589
this.#CRLF_BYTES_LENGTH = this.#CRLF_BYTES.byteLength
8690

87-
this.#form = form
91+
this.boundary = `form-data-boundary-${boundary}`
92+
this.contentType = `multipart/form-data; boundary=${this.boundary}`
93+
8894
this.#footer = this.#encoder.encode(
8995
`${this.#DASHES}${this.boundary}${this.#DASHES}${this.#CRLF.repeat(2)}`
9096
)
9197

98+
this.contentLength = String(this.getContentLength())
99+
92100
this.headers = Object.freeze({
93101
"Content-Type": this.contentType,
94-
"Content-Length": String(this.getContentLength())
102+
"Content-Length": this.contentLength
103+
})
104+
105+
// Make sure following properties read/only in runtime.
106+
Object.defineProperties(this, {
107+
boundary: {writable: false, configurable: false},
108+
contentType: {writable: false, configurable: false},
109+
contentLength: {writable: false, configurable: false},
110+
headers: {writable: false, configurable: false}
95111
})
96112
}
97113

0 commit comments

Comments
 (0)