Skip to content

Commit 71df00e

Browse files
Fix file resolver on redirect (#1870)
* fix redirects in file resolver, improve errors * add file resolver tests * fixes #1869
1 parent 5b312cf commit 71df00e

File tree

2 files changed

+132
-5
lines changed

2 files changed

+132
-5
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import os from 'node:os';
2+
import path from 'node:path';
3+
import fs from 'fs-extra';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
import { resolveFile } from './file-resolver';
6+
7+
// Mock fetch globally
8+
const mockFetch = vi.fn();
9+
global.fetch = mockFetch;
10+
11+
describe('resolveFile', () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
afterEach(async () => {
17+
// Clean up any temp directories that might have been created
18+
const dirs = await fs.readdir(os.tmpdir());
19+
for (const dir of dirs) {
20+
if (dir.startsWith('graph-file-')) {
21+
await fs.remove(path.join(os.tmpdir(), dir));
22+
}
23+
}
24+
});
25+
26+
it('should handle local files', async () => {
27+
const testFile = path.join(os.tmpdir(), 'test.txt');
28+
await fs.writeFile(testFile, 'test content');
29+
30+
const result = await resolveFile(testFile, 'test.txt');
31+
expect(result.path).toBe(testFile);
32+
expect(result.cleanup).toBeUndefined();
33+
34+
await fs.remove(testFile);
35+
});
36+
37+
it('should handle HTTP URLs with redirects', async () => {
38+
const testUrl = 'https://example.com/file.txt';
39+
const testContent = 'test content';
40+
41+
mockFetch.mockResolvedValueOnce({
42+
ok: true,
43+
arrayBuffer: async () => Buffer.from(testContent),
44+
});
45+
46+
const result = await resolveFile(testUrl, 'file.txt');
47+
48+
expect(mockFetch).toHaveBeenCalledWith(testUrl, { redirect: 'follow' });
49+
expect(result.path).toMatch(/graph-file-.*\/file\.txt$/);
50+
expect(result.cleanup).toBeDefined();
51+
expect(await fs.readFile(result.path, 'utf-8')).toBe(testContent);
52+
53+
if (result.cleanup) {
54+
result.cleanup();
55+
}
56+
});
57+
58+
it('should handle IPFS hashes', async () => {
59+
const ipfsHash = 'QmTest';
60+
const testContent = 'test content';
61+
62+
mockFetch.mockResolvedValueOnce({
63+
ok: true,
64+
arrayBuffer: async () => Buffer.from(testContent),
65+
});
66+
67+
const result = await resolveFile(ipfsHash, 'file.txt');
68+
69+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining(ipfsHash));
70+
expect(result.path).toMatch(/graph-file-.*\/file\.txt$/);
71+
expect(result.cleanup).toBeDefined();
72+
expect(await fs.readFile(result.path, 'utf-8')).toBe(testContent);
73+
74+
if (result.cleanup) {
75+
result.cleanup();
76+
}
77+
});
78+
79+
it('should handle failed HTTP fetches', async () => {
80+
const testUrl = 'https://example.com/file.txt';
81+
82+
mockFetch.mockResolvedValueOnce({
83+
ok: false,
84+
statusText: 'Not Found',
85+
});
86+
87+
await expect(resolveFile(testUrl, 'file.txt')).rejects.toThrow(
88+
`Failed to resolve ${testUrl} - failed to fetch from URL: Not Found`,
89+
);
90+
});
91+
92+
it('should handle network errors', async () => {
93+
const testUrl = 'https://example.com/file.txt';
94+
95+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
96+
97+
await expect(resolveFile(testUrl, 'file.txt')).rejects.toThrow(
98+
`Failed to resolve ${testUrl} - Network error`,
99+
);
100+
});
101+
102+
it('should handle timeout', async () => {
103+
const testUrl = 'https://example.com/file.txt';
104+
105+
mockFetch.mockImplementationOnce(() => new Promise(resolve => setTimeout(resolve, 2000)));
106+
107+
await expect(resolveFile(testUrl, 'file.txt', 100)).rejects.toThrow('File download timed out');
108+
});
109+
110+
it('should clean up temp files on error', async () => {
111+
const testUrl = 'https://example.com/file.txt';
112+
113+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
114+
115+
try {
116+
await resolveFile(testUrl, 'file.txt');
117+
} catch (e) {
118+
// Expected error
119+
}
120+
121+
const dirs = await fs.readdir(os.tmpdir());
122+
const graphTempDirs = dirs.filter(dir => dir.startsWith('graph-file-'));
123+
expect(graphTempDirs).toHaveLength(0);
124+
});
125+
});

packages/cli/src/command-helpers/file-resolver.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os from 'node:os';
22
import path from 'node:path';
33
import fs from 'fs-extra';
4-
import fetch from '../fetch.js';
54
import { DEFAULT_IPFS_URL } from './ipfs.js';
65

76
export interface FileSource {
@@ -42,7 +41,7 @@ export async function resolveFile(
4241
if (source.startsWith('Qm')) {
4342
const response = await fetch(`${DEFAULT_IPFS_URL}/cat?arg=${source}`);
4443
if (!response.ok) {
45-
throw new Error(`Failed to fetch file from IPFS: ${response.statusText}`);
44+
throw new Error(`failed to fetch from IPFS: ${response.statusText}`);
4645
}
4746
const filePath = path.join(tempDir, fileName);
4847
const buffer = Buffer.from(await response.arrayBuffer());
@@ -52,9 +51,9 @@ export async function resolveFile(
5251

5352
// If it's a URL
5453
if (source.startsWith('http')) {
55-
const response = await fetch(source);
54+
const response = await fetch(source, { redirect: 'follow' });
5655
if (!response.ok) {
57-
throw new Error(`Failed to fetch file from URL: ${response.statusText}`);
56+
throw new Error(`failed to fetch from URL: ${response.statusText}`);
5857
}
5958
const filePath = path.join(tempDir, fileName);
6059
const buffer = Buffer.from(await response.arrayBuffer());
@@ -66,7 +65,10 @@ export async function resolveFile(
6665
throw new Error('Invalid file source. Must be a file path, IPFS hash, or URL');
6766
} catch (error) {
6867
cleanup();
69-
throw error;
68+
if (error instanceof Error) {
69+
throw new Error(`Failed to resolve ${source} - ${error.message}`);
70+
}
71+
throw new Error(`Failed to resolve ${source}`);
7072
}
7173
};
7274

0 commit comments

Comments
 (0)