Skip to content

Commit

Permalink
fix(node-fetch): Body.clone
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jul 20, 2024
1 parent 483462f commit 1c5b838
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-coats-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/node-fetch': patch
---

Fix Request.clone and Response.clone
98 changes: 69 additions & 29 deletions packages/node-fetch/src/Body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class PonyfillBody<TJSON = any> implements Body {
private _buffer?: Buffer;

private generateBody(): PonyfillReadableStream<Uint8Array> | null {
if (this._generatedBody?.readable?.destroyed && this._buffer) {
this._generatedBody.readable = Readable.from(this._buffer);
}
if (this._generatedBody) {
return this._generatedBody;
}
Expand Down Expand Up @@ -101,52 +104,70 @@ export class PonyfillBody<TJSON = any> implements Body {
return null;
}

_chunks: Uint8Array[] | null = null;

_collectChunksFromReadable() {
if (this._chunks) {
return fakePromise(this._chunks);
}
const _body = this.generateBody();
if (!_body) {
return fakePromise([]);
}
const chunks: Uint8Array[] = [];
this._chunks = [];
_body.readable.on('data', chunk => {
chunks.push(chunk);
this._chunks!.push(chunk);
});
return new Promise<Uint8Array[]>((resolve, reject) => {
_body.readable.once('end', () => {
resolve(chunks);
resolve(this._chunks!);
});
_body.readable.once('error', e => {
reject(e);
});
});
}

_blob: PonyfillBlob | null = null;

blob(): Promise<PonyfillBlob> {
if (this._blob) {
return fakePromise(this._blob);
}
if (this.bodyType === BodyInitType.Blob) {
return fakePromise(this.bodyInit as PonyfillBlob);
this._blob = this.bodyInit as PonyfillBlob;
return fakePromise(this._blob);
}
if (this._buffer) {
const blob = new PonyfillBlob([this._buffer], {
this._blob = new PonyfillBlob([this._buffer], {
type: this.contentType || '',
size: this.contentLength,
});
return fakePromise(blob);
return fakePromise(this._blob);
}
return this._collectChunksFromReadable().then(chunks => {
return new PonyfillBlob(chunks, {
this._blob = new PonyfillBlob(chunks, {
type: this.contentType || '',
size: this.contentLength,
});
return this._blob;
});
}

_formData: PonyfillFormData | null = null;

formData(opts?: { formDataLimits: FormDataLimits }): Promise<PonyfillFormData> {
if (this._formData) {
return fakePromise(this._formData);
}
if (this.bodyType === BodyInitType.FormData) {
return fakePromise(this.bodyInit as PonyfillFormData);
this._formData = this.bodyInit as PonyfillFormData;
return fakePromise(this._formData);
}
const formData = new PonyfillFormData();
this._formData = new PonyfillFormData();
const _body = this.generateBody();
if (_body == null) {
return fakePromise(formData);
return fakePromise(this._formData);
}
const formDataLimits = {
...this.options.formDataLimits,
Expand All @@ -167,7 +188,7 @@ export class PonyfillBody<TJSON = any> implements Body {
if (valueTruncated) {
reject(new Error(`Field value size exceeded: ${formDataLimits?.fieldSize} bytes`));
}
formData.set(name, value);
this._formData!.set(name, value);
});
bb.on('fieldsLimit', () => {
reject(new Error(`Fields limit exceeded: ${formDataLimits?.fields}`));
Expand All @@ -187,7 +208,7 @@ export class PonyfillBody<TJSON = any> implements Body {
reject(new Error(`File size limit exceeded: ${formDataLimits?.fileSize} bytes`));
}
const file = new PonyfillFile(chunks, filename, { type: mimeType });
formData.set(name, file);
this._formData!.set(name, file);
});
},
);
Expand All @@ -198,7 +219,7 @@ export class PonyfillBody<TJSON = any> implements Body {
reject(new Error(`Parts limit exceeded: ${formDataLimits?.parts}`));
});
bb.on('close', () => {
resolve(formData);
resolve(this._formData!);
});
bb.on('error', (err: any = 'An error occurred while parsing the form data') => {
const errMessage = err.message || err.toString();
Expand All @@ -214,34 +235,53 @@ export class PonyfillBody<TJSON = any> implements Body {
}
if (this.bodyType === BodyInitType.Blob) {
if (this.bodyInit instanceof PonyfillBlob) {
return this.bodyInit.arrayBuffer();
return this.bodyInit.arrayBuffer().then(buf => {
this._buffer = buf as Buffer;
return this._buffer;
});
}
const bodyInitTyped = this.bodyInit as Blob;
return bodyInitTyped
.arrayBuffer()
.then(arrayBuffer => Buffer.from(arrayBuffer, undefined, bodyInitTyped.size));
return bodyInitTyped.arrayBuffer().then(arrayBuffer => {
this._buffer = Buffer.from(arrayBuffer, undefined, bodyInitTyped.size);
return this._buffer;
});
}
return this._collectChunksFromReadable().then(
function concatCollectedChunksFromReadable(chunks) {
if (chunks.length === 1) {
return chunks[0] as Buffer;
}
return Buffer.concat(chunks);
},
);
return this._collectChunksFromReadable().then(chunks => {
if (chunks.length === 1) {
this._buffer = chunks[0] as Buffer;
return this._buffer;
}
this._buffer = Buffer.concat(chunks);
return this._buffer;
});
}

_json: TJSON | null = null;

json(): Promise<TJSON> {
return this.text().then(function parseTextAsJson(text) {
return JSON.parse(text);
if (this._json) {
return fakePromise(this._json);
}
return this.text().then(text => {
this._json = JSON.parse(text);
return this._json!;
});
}

_text: string | null = null;

text(): Promise<string> {
if (this._text) {
return fakePromise(this._text);
}
if (this.bodyType === BodyInitType.String) {
return fakePromise(this.bodyInit as string);
this._text = this.bodyInit as string;
return fakePromise(this._text);
}
return this.arrayBuffer().then(buffer => buffer.toString('utf-8'));
return this.arrayBuffer().then(buffer => {
this._text = buffer.toString('utf-8');
return this._text;
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/node-fetch/src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class PonyfillRequest<TJSON = any> extends PonyfillBody<TJSON> implements
}

clone(): PonyfillRequest<TJSON> {
return new PonyfillRequest(this);
return this;
}

[Symbol.toStringTag] = 'Request';
Expand Down
2 changes: 1 addition & 1 deletion packages/node-fetch/src/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class PonyfillResponse<TJSON = any> extends PonyfillBody<TJSON> implement
type: ResponseType;

clone() {
return new PonyfillResponse(this.body, this);
return this;
}

static error() {
Expand Down
21 changes: 21 additions & 0 deletions packages/server/test/node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,27 @@ describe('Node Specific Cases', () => {
const resJson = await response.json();
expect(resJson.contentLength).toBe('0');
});

it('clones the request correctly', async () => {
const serverAdapter = createServerAdapter(async req => {
const clonedReq = req.clone();
const textFromClonedReq = await req.text();
const textFromOriginalReq = await clonedReq.text();
return Response.json({
textFromClonedReq,
textFromOriginalReq,
});
});
testServer.addOnceHandler(serverAdapter);
const response = await fetch(testServer.url, {
method: 'POST',
body: 'TEST',
});

const resJson = await response.json();
expect(resJson.textFromClonedReq).toBe('TEST');
expect(resJson.textFromOriginalReq).toBe('TEST');
});
});
},
);
Expand Down

0 comments on commit 1c5b838

Please sign in to comment.