Skip to content

Commit 5b5ae5f

Browse files
authored
feat(node-fetch): improve file access (#2009)
1 parent 11b893b commit 5b5ae5f

File tree

3 files changed

+57
-12
lines changed

3 files changed

+57
-12
lines changed

.changeset/tough-moles-arrive.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@whatwg-node/node-fetch': patch
3+
---
4+
5+
When `fetch('file:///...')` is used to read files;
6+
- 404 is returned if the file is missing
7+
- 403 is returned if the file is not accessible

packages/node-fetch/src/fetch.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Buffer } from 'node:buffer';
2-
import { createReadStream } from 'node:fs';
2+
import { createReadStream, promises as fsPromises } from 'node:fs';
33
import { fileURLToPath } from 'node:url';
44
import { fetchCurl } from './fetchCurl.js';
55
import { fetchNodeHttp } from './fetchNodeHttp.js';
@@ -10,10 +10,36 @@ import { fakePromise } from './utils.js';
1010

1111
const BASE64_SUFFIX = ';base64';
1212

13-
function getResponseForFile(url: string) {
13+
async function getResponseForFile(url: string) {
1414
const path = fileURLToPath(url);
15-
const readable = createReadStream(path);
16-
return new PonyfillResponse(readable);
15+
try {
16+
await fsPromises.access(path, fsPromises.constants.R_OK);
17+
const stats = await fsPromises.stat(path, {
18+
bigint: true,
19+
});
20+
const readable = createReadStream(path);
21+
return new PonyfillResponse(readable, {
22+
status: 200,
23+
statusText: 'OK',
24+
headers: {
25+
'content-type': 'application/octet-stream',
26+
'last-modified': stats.mtime.toUTCString(),
27+
},
28+
});
29+
} catch (err: any) {
30+
if (err.code === 'ENOENT') {
31+
return new PonyfillResponse(null, {
32+
status: 404,
33+
statusText: 'Not Found',
34+
});
35+
} else if (err.code === 'EACCES') {
36+
return new PonyfillResponse(null, {
37+
status: 403,
38+
statusText: 'Forbidden',
39+
});
40+
}
41+
throw err;
42+
}
1743
}
1844

1945
function getResponseForDataUri(url: string) {
@@ -73,7 +99,7 @@ export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
7399

74100
if (fetchRequest.url.startsWith('file:')) {
75101
const response = getResponseForFile(fetchRequest.url);
76-
return fakePromise(response);
102+
return response;
77103
}
78104
if (fetchRequest.url.startsWith('blob:')) {
79105
const response = getResponseForBlob(fetchRequest.url);

packages/node-fetch/tests/non-http-fetch.spec.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@ import { pathToFileURL } from 'node:url';
44
import { describe, expect, it } from '@jest/globals';
55
import { fetchPonyfill } from '../src/fetch.js';
66

7-
it('should respect file protocol', async () => {
8-
const response = await fetchPonyfill(
9-
pathToFileURL(join(process.cwd(), './packages/node-fetch/tests/fixtures/test.json')),
10-
);
11-
expect(response.status).toBe(200);
12-
const body = await response.json();
13-
expect(body.foo).toBe('bar');
7+
describe('File protocol', () => {
8+
it('reads', async () => {
9+
const response = await fetchPonyfill(
10+
pathToFileURL(join(process.cwd(), './packages/node-fetch/tests/fixtures/test.json')),
11+
);
12+
expect(response.status).toBe(200);
13+
const body = await response.json();
14+
expect(body.foo).toBe('bar');
15+
});
16+
it('returns 404 if file does not exist', async () => {
17+
const response = await fetchPonyfill(
18+
pathToFileURL(join(process.cwd(), './packages/node-fetch/tests/fixtures/missing.json')),
19+
);
20+
expect(response.status).toBe(404);
21+
});
22+
it('returns 403 if file is not accessible', async () => {
23+
const response = await fetchPonyfill(pathToFileURL('/root/private_data.txt'));
24+
expect(response.status).toBe(403);
25+
});
1426
});
1527

1628
describe('data uris', () => {

0 commit comments

Comments
 (0)