Skip to content

Commit

Permalink
fix(node-fetch): handle buffers correctly in blobs
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jul 25, 2024
1 parent 3c42236 commit cf9733e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-melons-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/node-fetch': patch
---

Handle Buffers correctly in Blobs
48 changes: 43 additions & 5 deletions packages/node-fetch/src/Blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,37 @@ function getBlobPartAsBuffer(blobPart: Exclude<BlobPart, Blob>) {
}

export function hasBufferMethod(obj: any): obj is { buffer(): Promise<Buffer> } {
return obj != null && obj.buffer != null;
return obj != null && obj.buffer != null && typeof obj.buffer === 'function';
}

export function hasArrayBufferMethod(obj: any): obj is { arrayBuffer(): Promise<ArrayBuffer> } {
return obj != null && obj.arrayBuffer != null;
return obj != null && obj.arrayBuffer != null && typeof obj.arrayBuffer === 'function';
}

export function hasBytesMethod(obj: any): obj is { bytes(): Promise<Uint8Array> } {
return obj != null && obj.bytes != null;
return obj != null && obj.bytes != null && typeof obj.bytes === 'function';
}

export function hasTextMethod(obj: any): obj is { text(): Promise<string> } {
return obj != null && obj.text != null;
return obj != null && obj.text != null && typeof obj.text === 'function';
}

export function hasSizeProperty(obj: any): obj is { size: number } {
return obj != null && typeof obj.size === 'number';
}

export function hasStreamMethod(obj: any): obj is { stream(): any } {
return obj != null && obj.stream != null;
return obj != null && obj.stream != null && typeof obj.stream === 'function';
}

export function hasBlobSignature(obj: any): obj is Blob {
return obj != null && obj[Symbol.toStringTag] === 'Blob';
}

export function isArrayBuffer(obj: any): obj is ArrayBuffer {
return obj != null && obj.byteLength != null && obj.slice != null;
}

// Will be removed after v14 reaches EOL
// Needed because v14 doesn't have .stream() implemented
export class PonyfillBlob implements Blob {
Expand Down Expand Up @@ -141,6 +145,40 @@ export class PonyfillBlob implements Blob {
}

arrayBuffer(): Promise<ArrayBuffer> {
if (this._buffer) {
return fakePromise(this._buffer);
}
if (this.blobParts.length === 1) {
if (isArrayBuffer(this.blobParts[0])) {
return fakePromise(this.blobParts[0]);
}
if (hasArrayBufferMethod(this.blobParts[0])) {
return this.blobParts[0].arrayBuffer();
}
}
return this.buffer();
}

bytes(): Promise<Uint8Array> {
if (this._buffer) {
return fakePromise(this._buffer);
}
if (this.blobParts.length === 1) {
if (Buffer.isBuffer(this.blobParts[0])) {
this._buffer = this.blobParts[0];
return fakePromise(this.blobParts[0]);
}
if (this.blobParts[0] instanceof Uint8Array) {
this._buffer = Buffer.from(this.blobParts[0]);
return fakePromise(this.blobParts[0]);
}
if (hasBytesMethod(this.blobParts[0])) {
return this.blobParts[0].bytes();
}
if (hasBufferMethod(this.blobParts[0])) {
return this.blobParts[0].buffer();
}
}
return this.buffer();
}

Expand Down
62 changes: 62 additions & 0 deletions packages/node-fetch/tests/Blob.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Blob as NodeBlob } from 'buffer';
import { isArrayBuffer, PonyfillBlob } from '../src/Blob';

describe('Blob', () => {
const blobParts: Record<string, BlobPart> = {
string: 'string',
globalBlob: new Blob(['globalBlob']),
nodeBlob: new NodeBlob(['nodeBlob']) as Blob,
arrayBuffer: Buffer.from('arrayBuffer'),
};
for (const [name, blobPart] of Object.entries(blobParts)) {
describe(name, () => {
describe('arrayBuffer', () => {
it('content', async () => {
const blob = new PonyfillBlob([blobPart]);
const buffer = await blob.arrayBuffer();
expect(isArrayBuffer(buffer)).toBe(true);
expect(Buffer.from(buffer, undefined, buffer.byteLength).toString('utf-8')).toBe(name);
});
it('size', async () => {
const blob = new PonyfillBlob([blobPart]);
const buffer = await blob.arrayBuffer();
expect(blob.size).toBe(buffer.byteLength);
});
});
describe('text', () => {
it('content', async () => {
const blob = new PonyfillBlob([blobPart]);
const text = await blob.text();
expect(typeof text).toBe('string');
expect(text).toBe(name);
});
it('size', async () => {
const blob = new PonyfillBlob([blobPart]);
const text = await blob.text();
expect(blob.size).toBe(Buffer.byteLength(text));
});
});
describe('stream', () => {
it('content', async () => {
const blob = new PonyfillBlob([blobPart]);
const stream = blob.stream();
expect(typeof stream[Symbol.asyncIterator]).toBe('function');
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
expect(Buffer.concat(chunks).toString('utf-8')).toBe(name);
});
it('size', async () => {
const blob = new PonyfillBlob([blobPart]);
const stream = blob.stream();
let size = 0;
for await (const chunk of stream) {
size += chunk.length;
}
expect(blob.size).toBe(size);
});
});
});
}
});

0 comments on commit cf9733e

Please sign in to comment.