Skip to content

Commit e156a6e

Browse files
MichaelDeBoeyjacob-ebeyGozala
authored
fix(fetch): fix issue with cloning bodies (#72)
* fix: issue with cloning bodies chore: updated packages to @remix-run/web * oops * fix: fix root cause of clone issue chore: revert remix-run#5 The issue was strings were being fed through the Uint8Array stream instead of being encoded. * add more tests * fix: cleanup imports --------- Co-authored-by: Jacob Ebey <[email protected]> Co-authored-by: Irakli Gozalishvili <[email protected]>
1 parent 9e95faf commit e156a6e

File tree

3 files changed

+64
-7
lines changed

3 files changed

+64
-7
lines changed

packages/fetch/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
"@types/mocha": "^9.1.0",
7878
"@types/chai-as-promised": "^7.1.5",
7979
"@types/chai-string": "^1.4.2",
80+
"abort-controller": "^3.0.0",
81+
"@web-std/file": "^3.0.2",
8082
"abortcontroller-polyfill": "^1.7.1",
8183
"busboy": "^0.3.1",
8284
"c8": "^7.3.0",

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,21 @@ export const getBoundary = () => randomBytes(8).toString('hex');
4444
* @param {string} boundary
4545
*/
4646
export async function * formDataIterator(form, boundary) {
47+
const encoder = new TextEncoder();
4748
for (const [name, value] of form) {
48-
yield getHeader(boundary, name, value);
49+
yield encoder.encode(getHeader(boundary, name, value));
4950

5051
if (isBlob(value)) {
5152
// @ts-ignore - we know our streams implement aysnc iteration
5253
yield * value.stream();
5354
} else {
54-
yield value;
55+
yield encoder.encode(value);
5556
}
5657

57-
yield carriage;
58+
yield encoder.encode(carriage);
5859
}
5960

60-
yield getFooter(boundary);
61+
yield encoder.encode(getFooter(boundary));
6162
}
6263

6364
/**

packages/fetch/test/request.js

+57-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import {TextEncoder} from 'util';
44
import AbortController from 'abort-controller';
55
import chai from 'chai';
66
import FormData from 'form-data';
7-
import {Blob} from '@web-std/fetch';
8-
import { ReadableStream } from '@web-std/fetch';
7+
import { Blob, ReadableStream, Request, FormData as WebFormData} from '@web-std/fetch';
8+
import { File } from '@web-std/file';
99

1010
import TestServer from './utils/server.js';
11-
import {Request} from '@web-std/fetch';
1211

1312
const {expect} = chai;
1413

@@ -19,6 +18,7 @@ describe('Request', () => {
1918
before(async () => {
2019
await local.start();
2120
base = `http://${local.hostname}:${local.port}/`;
21+
global.File = File;
2222
});
2323

2424
after(async () => {
@@ -384,4 +384,58 @@ describe('Request', () => {
384384
expect(result).to.equal('a=1');
385385
});
386386
});
387+
388+
it('should read formData after clone with web FormData body',async () => {
389+
const ogFormData = new WebFormData();
390+
ogFormData.append('a', 1);
391+
ogFormData.append('b', 2);
392+
ogFormData.append('file', new File(['content'], 'file.txt'));
393+
394+
const request = new Request(base, {
395+
method: 'POST',
396+
body: ogFormData,
397+
});
398+
const clonedRequest = request.clone();
399+
400+
return clonedRequest.formData().then(async clonedFormData => {
401+
expect(clonedFormData.get('a')).to.equal("1");
402+
expect(clonedFormData.get('b')).to.equal("2");
403+
const file = clonedFormData.get('file')
404+
if (typeof file !== "object") {
405+
throw new Error("File is not an object");
406+
}
407+
expect(file.name).to.equal("file.txt");
408+
expect(file.type).to.equal("application/octet-stream");
409+
expect(file.size).to.equal(7);
410+
expect(await file.text()).to.equal("content");
411+
expect(file.lastModified).to.be.a('number');
412+
});
413+
});
414+
415+
it('should read formData after clone with node FormData body',async () => {
416+
const ogFormData = new FormData();
417+
ogFormData.append('a', '1');
418+
ogFormData.append('b', '2');
419+
ogFormData.append('file', Buffer.from('content'), { filename: "file.txt" });
420+
421+
const request = new Request(base, {
422+
method: 'POST',
423+
body: ogFormData,
424+
});
425+
const clonedRequest = request.clone();
426+
427+
return clonedRequest.formData().then(async clonedFormData => {
428+
expect(clonedFormData.get('a')).to.equal("1");
429+
expect(clonedFormData.get('b')).to.equal("2");
430+
const file = clonedFormData.get('file')
431+
if (typeof file !== "object") {
432+
throw new Error("File is not an object");
433+
}
434+
expect(file.name).to.equal("file.txt");
435+
expect(file.type).to.equal("text/plain");
436+
expect(file.size).to.equal(7);
437+
expect(await file.text()).to.equal("content");
438+
expect(file.lastModified).to.be.a('number');
439+
});
440+
});
387441
});

0 commit comments

Comments
 (0)