Skip to content

Commit 1f1afb7

Browse files
Copilotstreamich
andcommitted
feat: align openAsBlob errors with Node.js behavior
Co-authored-by: streamich <[email protected]>
1 parent 6892aa3 commit 1f1afb7

File tree

5 files changed

+51
-13
lines changed

5 files changed

+51
-13
lines changed

src/fsa-to-node/FsaNodeFs.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { FsaNodeReadStream } from './FsaNodeReadStream';
1717
import { FsaNodeCore } from './FsaNodeCore';
1818
import { FileHandle } from '../node/FileHandle';
1919
import { dataToBuffer, isFd, isWin, validateFd } from '../core/util';
20+
import * as errors from '../vendor/node/internal/errors';
2021
import type { FsCallbackApi, FsPromisesApi } from '../node/types';
2122
import type * as misc from '../node/types/misc';
2223
import type * as opts from '../node/types/options';
@@ -792,12 +793,22 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
792793
};
793794

794795
public openAsBlob = async (path: misc.PathLike, options?: opts.IOpenAsBlobOptions): Promise<Blob> => {
795-
const buffer = await new Promise<Buffer>((resolve, reject) => {
796-
this.readFile(path, (err, data: Buffer) => {
797-
if (err) reject(err);
798-
else resolve(data);
796+
let buffer;
797+
try {
798+
buffer = await new Promise<Buffer>((resolve, reject) => {
799+
this.readFile(path, (err, data: Buffer) => {
800+
if (err) reject(err);
801+
else resolve(data);
802+
});
799803
});
800-
});
804+
} catch (error) {
805+
// Convert ENOENT to Node.js-compatible error for openAsBlob
806+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
807+
const nodeError = new errors.TypeError('ERR_INVALID_ARG_VALUE');
808+
throw nodeError;
809+
}
810+
throw error;
811+
}
801812
const type = options?.type || '';
802813
return new Blob([buffer as BlobPart], { type });
803814
};

src/fsa-to-node/__tests__/FsaNodeFs.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,18 @@ onlyOnNode20('FsaNodeFs', () => {
303303
const blob = await fs.openAsBlob('/folder/file');
304304
expect(await blob.text()).toBe('test');
305305
});
306+
307+
test('throws TypeError with ERR_INVALID_ARG_VALUE for non-existing files', async () => {
308+
const { fs } = setup({ folder: { file: 'test' }, 'empty-folder': null });
309+
try {
310+
await fs.openAsBlob('/nonexistent');
311+
throw new Error('Expected error');
312+
} catch (error) {
313+
expect(error).toBeInstanceOf(TypeError);
314+
expect(error.code).toBe('ERR_INVALID_ARG_VALUE');
315+
expect(error.message).toContain('Unable to open file as blob');
316+
}
317+
});
306318
});
307319

308320
describe('.truncate()', () => {

src/node/__tests__/volume/openAsBlob.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,17 @@ describe('.openAsBlob()', () => {
4848
it('throws if file does not exist', async () => {
4949
const { fs } = memfs({ '/dir/test.txt': 'content' });
5050
const [, err] = await of(fs.openAsBlob('/dir/test-NOT-FOUND.txt'));
51-
expect(err).toBeInstanceOf(Error);
52-
expect((<any>err).code).toBe('ENOENT');
51+
expect(err).toBeInstanceOf(TypeError);
52+
expect((<any>err).code).toBe('ERR_INVALID_ARG_VALUE');
53+
expect((<any>err).message).toContain('Unable to open file as blob');
5354
});
5455

55-
it('throws EISDIR if path is a directory', async () => {
56+
it('allows opening directories as blobs like Node.js', async () => {
5657
const { fs } = memfs({ '/dir/test.txt': 'content' });
57-
const [, err] = await of(fs.openAsBlob('/dir'));
58-
expect(err).toBeInstanceOf(Error);
59-
expect((<any>err).code).toBe('EISDIR');
58+
const blob = await fs.openAsBlob('/dir');
59+
expect(blob).toBeInstanceOf(Blob);
60+
// Directory "content" size may vary, but it should be a valid blob
61+
expect(typeof blob.size).toBe('number');
6062
});
6163

6264
it('works with Buffer paths', async () => {

src/node/volume.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { FsCallbackApi, WritevCallback } from './types/FsCallbackApi';
1919
import { FsPromises } from './FsPromises';
2020
import { ToTreeOptions, toTreeSync } from '../print';
2121
import { ERRSTR, FLAGS, MODE } from './constants';
22+
import * as errors from '../vendor/node/internal/errors';
2223
import {
2324
getDefaultOpts,
2425
getDefaultOptsAndCb,
@@ -1489,9 +1490,20 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
14891490

14901491
public openAsBlob = async (path: PathLike, options?: opts.IOpenAsBlobOptions): Promise<Blob> => {
14911492
const filename = pathToFilename(path);
1492-
const link = this._core.getResolvedLinkOrThrow(filename, 'open');
1493+
let link;
1494+
try {
1495+
link = this._core.getResolvedLinkOrThrow(filename, 'open');
1496+
} catch (error) {
1497+
// Convert ENOENT to Node.js-compatible error for openAsBlob
1498+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
1499+
const nodeError = new errors.TypeError('ERR_INVALID_ARG_VALUE');
1500+
throw nodeError;
1501+
}
1502+
throw error;
1503+
}
1504+
14931505
const node = link.getNode();
1494-
if (node.isDirectory()) throw createError(ERROR_CODE.EISDIR, 'open', link.getPath());
1506+
// Note: Node.js allows opening directories as blobs, so we don't throw EISDIR
14951507

14961508
const buffer = node.getBuffer();
14971509
const type = options?.type || '';

src/vendor/node/internal/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ E('ERR_INVALID_OPT_VALUE', (name, value) => {
8686
return `The value "${String(value)}" is invalid for option "${name}"`;
8787
});
8888
E('ERR_INVALID_OPT_VALUE_ENCODING', value => `The value "${String(value)}" is invalid for option "encoding"`);
89+
E('ERR_INVALID_ARG_VALUE', 'Unable to open file as blob');

0 commit comments

Comments
 (0)