Skip to content

Commit dbe3f91

Browse files
committed
Fix eslint issue
1 parent 111c7e5 commit dbe3f91

File tree

6 files changed

+1032
-618
lines changed

6 files changed

+1032
-618
lines changed

lib/FormDataEncoder.test.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ test("Has contentLength property", async t => {
106106
)
107107
})
108108

109+
test(
110+
"contentLength property is undefined if there's file without known length",
111+
112+
t => {
113+
const form = new FormData()
114+
115+
form.set("stream", {
116+
[Symbol.toStringTag]: "File",
117+
name: "file.txt",
118+
stream() {
119+
return Readable.from([Buffer.from("foo")])
120+
}
121+
})
122+
123+
const encoder = new FormDataEncoder(form)
124+
125+
t.is(encoder.contentLength, undefined)
126+
}
127+
)
128+
109129
test("contentLength property is read-only", t => {
110130
const encoder = new FormDataEncoder(new FormData())
111131

@@ -137,6 +157,28 @@ test("Has correct headers", async t => {
137157
})
138158
})
139159

160+
test(
161+
"Has only Content-Type header if there's file without known length",
162+
163+
t => {
164+
const form = new FormData()
165+
166+
form.set("stream", {
167+
[Symbol.toStringTag]: "File",
168+
name: "file.txt",
169+
stream() {
170+
return Readable.from([Buffer.from("foo")])
171+
}
172+
})
173+
174+
const encoder = new FormDataEncoder(form)
175+
176+
t.deepEqual(encoder.headers, {
177+
"Content-Type": `multipart/form-data; boundary=${encoder.boundary}`
178+
})
179+
}
180+
)
181+
140182
test("Yields correct footer for empty FormData", async t => {
141183
const encoder = new FormDataEncoder(new FormData())
142184

@@ -157,7 +199,7 @@ test("Returns correct length of the empty FormData content", async t => {
157199
const encoder = new FormDataEncoder(new FormData())
158200
const expected = await readStream(encoder).then(({length}) => length)
159201

160-
t.is<number>(encoder.getContentLength(), expected)
202+
t.is(encoder.getContentLength(), expected)
161203
})
162204

163205
test("Returns the length of the FormData content", async t => {
@@ -170,9 +212,29 @@ test("Returns the length of the FormData content", async t => {
170212

171213
const expected = await readStream(encoder).then(({length}) => length)
172214

173-
t.is<number>(encoder.getContentLength(), expected)
215+
t.is(encoder.getContentLength(), expected)
174216
})
175217

218+
test(
219+
".getContentLength() returns undefined if there's file without known length",
220+
221+
t => {
222+
const form = new FormData()
223+
224+
form.set("stream", {
225+
[Symbol.toStringTag]: "File",
226+
name: "file.txt",
227+
stream() {
228+
return Readable.from([Buffer.from("foo")])
229+
}
230+
})
231+
232+
const encoder = new FormDataEncoder(form)
233+
234+
t.is(encoder.getContentLength(), undefined)
235+
}
236+
)
237+
176238
test(".values() yields headers as Uint8Array", t => {
177239
const form = new FormData()
178240

@@ -329,6 +391,41 @@ test(
329391
}
330392
)
331393

394+
test(
395+
"Does not imclude Content-Length header with enableAdditionalHeaders "
396+
+ "option if entry does not have known length",
397+
398+
async t => {
399+
const form = new FormData()
400+
401+
form.set("stream", {
402+
[Symbol.toStringTag]: "File",
403+
name: "file.txt",
404+
stream() {
405+
return Readable.from([Buffer.from("foo")])
406+
}
407+
})
408+
409+
const encoder = new FormDataEncoder(form, {
410+
enableAdditionalHeaders: true
411+
})
412+
413+
const iterable = readLine(Readable.from(encoder))
414+
415+
await skip(iterable, 1)
416+
const headers: string[] = []
417+
for await (const chunk of iterable) {
418+
if (chunk === "") {
419+
break
420+
}
421+
422+
headers.push(chunk.split(":")[0].toLowerCase())
423+
}
424+
425+
t.false(headers.includes("content-length"))
426+
}
427+
)
428+
332429
test("Yields File's content", async t => {
333430
const filePath = "license"
334431
const form = new FormData()
@@ -354,7 +451,7 @@ test("Yields File's content", async t => {
354451
chunks.pop() // Remove trailing empty line
355452

356453
// It looks like files on Windows have different EOL when you read them, even though they were created on macOS x)
357-
t.is<string>(chunks.join(EOL), expected)
454+
t.is(chunks.join(EOL), expected)
358455
})
359456

360457
test("Yields every appended field", async t => {

lib/FormDataEncoder.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable no-restricted-globals */
2+
13
import createBoundary from "./util/createBoundary"
24
import isPlainObject from "./util/isPlainObject"
35
import normalize from "./util/normalizeValue"
@@ -8,6 +10,11 @@ import {isFormData} from "./util/isFormData"
810
import {FormDataLike} from "./FormDataLike"
911
import {FileLike} from "./FileLike"
1012

13+
interface Headers {
14+
"Content-Type": string
15+
"Content-Length"?: string
16+
}
17+
1118
export interface FormDataEncoderOptions {
1219
/**
1320
* When enabled, the encoder will emit additional per part headers, such as `Content-Length`.
@@ -66,15 +73,12 @@ export class FormDataEncoder {
6673
/**
6774
* Returns Content-Length header
6875
*/
69-
readonly contentLength: string
76+
readonly contentLength: string | undefined
7077

7178
/**
7279
* Returns headers object with Content-Type and Content-Length header
7380
*/
74-
readonly headers: {
75-
"Content-Type": string
76-
"Content-Length": string
77-
}
81+
readonly headers: Readonly<Headers>
7882

7983
/**
8084
* Creates a multipart/form-data encoder.
@@ -163,12 +167,17 @@ export class FormDataEncoder {
163167
`${this.#DASHES}${this.boundary}${this.#DASHES}${this.#CRLF.repeat(2)}`
164168
)
165169

166-
this.contentLength = String(this.getContentLength())
170+
const contentLength = this.getContentLength()
171+
const headers: Headers = {
172+
"Content-Type": this.contentType
173+
}
167174

168-
this.headers = Object.freeze({
169-
"Content-Type": this.contentType,
170-
"Content-Length": this.contentLength
171-
})
175+
if (contentLength != null) {
176+
this.contentLength = String(contentLength)
177+
headers["Content-Length"] = this.contentLength
178+
}
179+
180+
this.headers = Object.freeze(headers)
172181

173182
// Make sure following properties read-only in runtime.
174183
Object.defineProperties(this, {
@@ -191,9 +200,10 @@ export class FormDataEncoder {
191200
}
192201

193202
if (this.#options.enableAdditionalHeaders === true) {
194-
header += `${this.#CRLF}Content-Length: ${
195-
isFileLike(value) ? value.size : value.byteLength
196-
}`
203+
const size = isFileLike(value) ? value.size : value.byteLength
204+
if (size != null && !isNaN(size)) {
205+
header += `${this.#CRLF}Content-Length: ${size}`
206+
}
197207
}
198208

199209
return this.#encoder.encode(`${header}${this.#CRLF.repeat(2)}`)
@@ -202,15 +212,22 @@ export class FormDataEncoder {
202212
/**
203213
* Returns form-data content length
204214
*/
205-
getContentLength(): number {
215+
getContentLength(): number | undefined {
206216
let length = 0
207217

208218
for (const [name, raw] of this.#form) {
209219
const value = isFileLike(raw) ? raw : this.#encoder.encode(normalize(raw))
210220

221+
const size = isFileLike(value) ? value.size : value.byteLength
222+
223+
// Return `undefined` if encountered part without known size
224+
if (size == null || isNaN(size)) {
225+
return undefined
226+
}
227+
211228
length += this.#getFieldHeader(name, value).byteLength
212229

213-
length += isFileLike(value) ? value.size : value.byteLength
230+
length += size
214231

215232
length += this.#CRLF_BYTES_LENGTH
216233
}

lib/util/isFileLike.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ test("Returns true for a file-shaped object", t => {
5656
t.true(isFileLike(object))
5757
})
5858

59+
test("Returns true for minimal valid file-ish object shape", t => {
60+
// eslint-disable-next-line max-len
61+
const object: Pick<FileLike, "name" | "stream" | typeof Symbol.toStringTag> = {
62+
name: "",
63+
64+
async* stream(): AsyncGenerator<Uint8Array> {
65+
yield new Uint8Array(0)
66+
},
67+
68+
get [Symbol.toStringTag](): string {
69+
return "File"
70+
}
71+
}
72+
73+
t.true(isFileLike(object))
74+
})
75+
5976
test("Returns false for null", t => {
6077
t.false(isFileLike(null))
6178
})

lib/util/isFileLike.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ import {FileLike} from "../FileLike"
2828
* isFileLike(fs.createReadStream("path/to/a/file.txt")) // -> false
2929
* ```
3030
*/
31-
export const isFileLike = (value?: unknown): value is FileLike => Boolean(
31+
export const isFileLike = (value: unknown): value is FileLike => Boolean(
3232
(value as FileLike)
3333
&& typeof (value as FileLike) === "object"
3434
&& isFunction((value as FileLike).constructor)
3535
&& (value as FileLike)[Symbol.toStringTag] === "File"
3636
&& isFunction((value as FileLike).stream)
3737
&& (value as FileLike).name != null
38-
&& (value as FileLike).size != null
39-
&& (value as FileLike).lastModified != null
4038
)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030
"build:types": "ttsc --project tsconfig.d.ts.json",
3131
"build": "npm run build:esm && npm run build:cjs && npm run build:types",
3232
"test": "ava --fail-fast",
33-
"cleanup": "npx rimraf @type \"lib/**/*.js\"",
34-
"prepare": "npm run cleanup && npm run build",
33+
"cleanup": "npx del-cli @type \"lib/**/*.js\"",
3534
"postinstall": "husky install",
3635
"prepublishOnly": "pinst --disable",
3736
"postpublish": "pinst --enable"
@@ -47,6 +46,7 @@
4746
"@zoltu/typescript-transformer-append-js-extension": "1.0.1",
4847
"ava": "4.2.0",
4948
"c8": "7.11.2",
49+
"del-cli": "5.0.0",
5050
"eslint": "7.32.0",
5151
"eslint-config-airbnb-typescript": "12.3.1",
5252
"eslint-plugin-ava": "12.0.0",

0 commit comments

Comments
 (0)