Skip to content

Commit 6521895

Browse files
cwohlmanjacob-ebey
andauthored
Fix submitting form data when file input is empty (#12)
Co-authored-by: Jacob Ebey <[email protected]>
1 parent dcfcac4 commit 6521895

File tree

3 files changed

+85
-68
lines changed

3 files changed

+85
-68
lines changed

.changeset/lemon-nails-rhyme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@remix-run/web-form-data": patch
3+
---
4+
5+
Fix submitting form data when file input is empty. Addresses https://github.com/remix-run/remix/pull/3576
6+

packages/form-data/src/form-data.js

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ export class FormData {
1313
)
1414
: new TypeError(
1515
"FormData constructor: Argument 1 does not implement interface HTMLFormElement."
16-
)
16+
);
1717

18-
throw error
18+
throw error;
1919
}
2020

2121
/**
2222
* @private
2323
* @readonly
2424
* @type {Array<[string, FormDataEntryValue]>}
2525
*/
26-
this._entries = []
26+
this._entries = [];
2727

28-
Object.defineProperty(this, "_entries", { enumerable: false })
28+
Object.defineProperty(this, "_entries", { enumerable: false });
2929
}
3030
get [Symbol.toStringTag]() {
31-
return "FormData"
31+
return "FormData";
3232
}
3333

3434
/**
@@ -54,7 +54,7 @@ export class FormData {
5454
),
5555
filename
5656
) {
57-
this._entries.push([name, toEntryValue(value, filename)])
57+
this._entries.push([name, toEntryValue(value, filename)]);
5858
}
5959

6060
/**
@@ -65,16 +65,16 @@ export class FormData {
6565
delete(
6666
name = panic(new TypeError("FormData.delete: requires string argument"))
6767
) {
68-
const entries = this._entries
69-
let index = 0
68+
const entries = this._entries;
69+
let index = 0;
7070
while (index < entries.length) {
7171
const [entryName] = /** @type {[string, FormDataEntryValue]}*/ (
7272
entries[index]
73-
)
73+
);
7474
if (entryName === name) {
75-
entries.splice(index, 1)
75+
entries.splice(index, 1);
7676
} else {
77-
index++
77+
index++;
7878
}
7979
}
8080
}
@@ -90,10 +90,10 @@ export class FormData {
9090
get(name = panic(new TypeError("FormData.get: requires string argument"))) {
9191
for (const [entryName, value] of this._entries) {
9292
if (entryName === name) {
93-
return value
93+
return value;
9494
}
9595
}
96-
return null
96+
return null;
9797
}
9898

9999
/**
@@ -106,13 +106,13 @@ export class FormData {
106106
getAll(
107107
name = panic(new TypeError("FormData.getAll: requires string argument"))
108108
) {
109-
const values = []
109+
const values = [];
110110
for (const [entryName, value] of this._entries) {
111111
if (entryName === name) {
112-
values.push(value)
112+
values.push(value);
113113
}
114114
}
115-
return values
115+
return values;
116116
}
117117

118118
/**
@@ -124,10 +124,10 @@ export class FormData {
124124
has(name = panic(new TypeError("FormData.has: requires string argument"))) {
125125
for (const [entryName] of this._entries) {
126126
if (entryName === name) {
127-
return true
127+
return true;
128128
}
129129
}
130-
return false
130+
return false;
131131
}
132132

133133
/**
@@ -144,27 +144,27 @@ export class FormData {
144144
value = panic(new TypeError("FormData.set: requires at least 2 arguments")),
145145
filename
146146
) {
147-
let index = 0
148-
const { _entries: entries } = this
149-
const entryValue = toEntryValue(value, filename)
150-
let wasSet = false
147+
let index = 0;
148+
const { _entries: entries } = this;
149+
const entryValue = toEntryValue(value, filename);
150+
let wasSet = false;
151151
while (index < entries.length) {
152-
const entry = /** @type {[string, FormDataEntryValue]}*/ (entries[index])
152+
const entry = /** @type {[string, FormDataEntryValue]}*/ (entries[index]);
153153
if (entry[0] === name) {
154154
if (wasSet) {
155-
entries.splice(index, 1)
155+
entries.splice(index, 1);
156156
} else {
157-
wasSet = true
158-
entry[1] = entryValue
159-
index++
157+
wasSet = true;
158+
entry[1] = entryValue;
159+
index++;
160160
}
161161
} else {
162-
index++
162+
index++;
163163
}
164164
}
165165

166166
if (!wasSet) {
167-
entries.push([name, entryValue])
167+
entries.push([name, entryValue]);
168168
}
169169
}
170170

@@ -173,7 +173,7 @@ export class FormData {
173173
* contained in this object.
174174
*/
175175
entries() {
176-
return this._entries.values()
176+
return this._entries.values();
177177
}
178178

179179
/**
@@ -184,7 +184,7 @@ export class FormData {
184184
*/
185185
*keys() {
186186
for (const [name] of this._entries) {
187-
yield name
187+
yield name;
188188
}
189189
}
190190

@@ -196,12 +196,12 @@ export class FormData {
196196
*/
197197
*values() {
198198
for (const [_, value] of this._entries) {
199-
yield value
199+
yield value;
200200
}
201201
}
202202

203203
[Symbol.iterator]() {
204-
return this._entries.values()
204+
return this._entries.values();
205205
}
206206

207207
/**
@@ -211,7 +211,7 @@ export class FormData {
211211
*/
212212
forEach(fn, thisArg) {
213213
for (const [key, value] of this._entries) {
214-
fn.call(thisArg, value, key, this)
214+
fn.call(thisArg, value, key, this);
215215
}
216216
}
217217
}
@@ -220,8 +220,8 @@ export class FormData {
220220
* @param {any} value
221221
* @returns {value is HTMLFormElement}
222222
*/
223-
const isHTMLFormElement = value =>
224-
Object.prototype.toString.call(value) === "[object HTMLFormElement]"
223+
const isHTMLFormElement = (value) =>
224+
Object.prototype.toString.call(value) === "[object HTMLFormElement]";
225225

226226
/**
227227
* @param {string|Blob|File} value
@@ -230,33 +230,33 @@ const isHTMLFormElement = value =>
230230
*/
231231
const toEntryValue = (value, filename) => {
232232
if (isFile(value)) {
233-
return filename != null ? new BlobFile([value], filename, value) : value
233+
return filename != null ? new BlobFile([value], filename, value) : value;
234234
} else if (isBlob(value)) {
235-
return new BlobFile([value], filename != null ? filename : "blob")
235+
return new BlobFile([value], filename != null ? filename : "blob");
236236
} else {
237-
if (filename != null) {
237+
if (filename != null && filename != "") {
238238
throw new TypeError(
239239
"filename is only supported when value is Blob or File"
240-
)
240+
);
241241
}
242-
return `${value}`
242+
return `${value}`;
243243
}
244-
}
244+
};
245245

246246
/**
247247
* @param {any} value
248248
* @returns {value is File}
249249
*/
250-
const isFile = value =>
250+
const isFile = (value) =>
251251
Object.prototype.toString.call(value) === "[object File]" &&
252-
typeof value.name === "string"
252+
typeof value.name === "string";
253253

254254
/**
255255
* @param {any} value
256256
* @returns {value is Blob}
257257
*/
258-
const isBlob = value =>
259-
Object.prototype.toString.call(value) === "[object Blob]"
258+
const isBlob = (value) =>
259+
Object.prototype.toString.call(value) === "[object Blob]";
260260

261261
/**
262262
* Simple `File` implementation that just wraps a given blob.
@@ -269,18 +269,18 @@ const BlobFile = class File {
269269
* @param {FilePropertyBag} [options]
270270
*/
271271
constructor([blob], name, { lastModified = Date.now() } = {}) {
272-
this.blob = blob
273-
this.name = name
274-
this.lastModified = lastModified
272+
this.blob = blob;
273+
this.name = name;
274+
this.lastModified = lastModified;
275275
}
276276
get webkitRelativePath() {
277-
return ""
277+
return "";
278278
}
279279
get size() {
280-
return this.blob.size
280+
return this.blob.size;
281281
}
282282
get type() {
283-
return this.blob.type
283+
return this.blob.type;
284284
}
285285
/**
286286
*
@@ -289,26 +289,26 @@ const BlobFile = class File {
289289
* @param {string} [contentType]
290290
*/
291291
slice(start, end, contentType) {
292-
return this.blob.slice(start, end, contentType)
292+
return this.blob.slice(start, end, contentType);
293293
}
294294
stream() {
295-
return this.blob.stream()
295+
return this.blob.stream();
296296
}
297297
text() {
298-
return this.blob.text()
298+
return this.blob.text();
299299
}
300300
arrayBuffer() {
301-
return this.blob.arrayBuffer()
301+
return this.blob.arrayBuffer();
302302
}
303303
get [Symbol.toStringTag]() {
304-
return "File"
304+
return "File";
305305
}
306-
}
306+
};
307307

308308
/**
309309
* @param {*} error
310310
* @returns {never}
311311
*/
312-
const panic = error => {
313-
throw error
314-
}
312+
const panic = (error) => {
313+
throw error;
314+
};

packages/form-data/test/form-data.spec.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { assert } from "./test.js";
66
/**
77
* @param {import('./test').Test} test
88
*/
9-
export const test = test => {
9+
export const test = (test) => {
1010
test("test baisc", async () => {
1111
assert.equal(typeof FormData, "function");
1212
assert.isEqual(typeof lib.FormData, "function");
@@ -89,6 +89,17 @@ export const test = test => {
8989
assert.equal(file2.lastModified, 123, "lastModified should be 123");
9090
});
9191

92+
// This mimics the payload sent by a browser when a file input
93+
// exists but is not filled out.
94+
test("filename on string contents", () => {
95+
const formData = new FormData();
96+
formData.set("file-3", new Blob([]), "");
97+
const file3 = /** @type {File} */ (formData.get("file-3"));
98+
assert.equal(file3.constructor.name, "File");
99+
assert.equal(file3.name, "");
100+
assert.equal(file3.type, "");
101+
});
102+
92103
test("throws on few args", () => {
93104
const data = new FormData();
94105
// @ts-expect-error
@@ -150,7 +161,7 @@ export const test = test => {
150161
["keyA", "val1"],
151162
["keyA", "val2"],
152163
["keyB", "val3"],
153-
["keyA", "val4"]
164+
["keyA", "val4"],
154165
]
155166
);
156167
});
@@ -167,7 +178,7 @@ export const test = test => {
167178
[...data],
168179
[
169180
["keyA", "val3"],
170-
["keyB", "val3"]
181+
["keyB", "val3"],
171182
]
172183
);
173184
});
@@ -181,7 +192,7 @@ export const test = test => {
181192
[...data],
182193
[
183194
["keyB", "val3"],
184-
["keyA", "val3"]
195+
["keyA", "val3"],
185196
]
186197
);
187198
});
@@ -207,21 +218,21 @@ export const test = test => {
207218
assert.deepEqual([...data], [["n2", "v2"]]);
208219
});
209220

210-
test("Shold return correct filename with File", () => {
221+
test("Should return correct filename with File", () => {
211222
const data = new FormData();
212223
data.set("key", new File([], "doc.txt"));
213224
const file = /** @type {File} */ (data.get("key"));
214225
assert.equal("doc.txt", file.name);
215226
});
216227

217-
test("Shold return correct filename with Blob filename", () => {
228+
test("Should return correct filename with Blob filename", () => {
218229
const data = new FormData();
219230
data.append("key", new Blob(), "doc.txt");
220231
const file = /** @type {File} */ (data.get("key"));
221232
assert.equal("doc.txt", file.name);
222233
});
223234

224-
test("Shold return correct filename with just Blob", () => {
235+
test("Should return correct filename with just Blob", () => {
225236
const data = new FormData();
226237
data.append("key", new Blob());
227238
const file = /** @type {File} */ (data.get("key"));

0 commit comments

Comments
 (0)