-
Notifications
You must be signed in to change notification settings - Fork 267
Expand file tree
/
Copy pathfetchBuffer.ts
More file actions
149 lines (122 loc) · 3.91 KB
/
Copy pathfetchBuffer.ts
File metadata and controls
149 lines (122 loc) · 3.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { net, type IncomingMessage } from 'electron';
import type Headers from '../../@types/Headers';
import { toFetchableUrl } from './ipfsGateway';
import isValidURL from './isValidURL';
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
const DEFAULT_MAX_SIZE = 100 * 1024 * 1024; // 100 MB
export type FetchBufferResult = {
data: Buffer;
headers: Headers;
};
/** Carries the response headers so callers can decide how to degrade when a
* response is too large to buffer — e.g. fall back to a direct URL for a
* verified image content type. */
export class MaxSizeExceededError extends Error {
readonly headers: Headers;
constructor(headers: Headers) {
super('Response exceeded maximum allowed size');
this.name = 'MaxSizeExceededError';
this.headers = headers;
}
}
export default async function fetchBuffer(
url: string,
options?: {
headers?: Record<string, string>;
timeout?: number;
maxSize?: number;
},
): Promise<FetchBufferResult> {
const { headers = {}, timeout = DEFAULT_TIMEOUT, maxSize = DEFAULT_MAX_SIZE } = options ?? {};
if (!isValidURL(url)) {
throw new Error('Invalid URL');
}
const request = net.request({
method: 'GET',
// ipfs:// URIs are fetched through an HTTPS gateway when the user has
// enabled it — Electron's net stack cannot request the ipfs scheme, and
// with the option off toFetchableUrl refuses the fetch outright.
url: toFetchableUrl(url),
headers,
});
return new Promise<FetchBufferResult>((resolve, reject) => {
let settled = false;
let timeoutId: NodeJS.Timeout | undefined;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
};
const resolveOnce = (result: FetchBufferResult) => {
if (!settled) {
settled = true;
cleanup();
resolve(result);
}
};
const rejectOnce = (error: Error) => {
if (!settled) {
settled = true;
cleanup();
reject(error);
}
};
const abortWith = (error: Error) => {
rejectOnce(error);
request.abort();
};
timeoutId = setTimeout(() => {
abortWith(new Error(`Request timeout after ${timeout}ms`));
}, timeout);
request.on('response', (response: IncomingMessage) => {
const { statusCode } = response;
if (statusCode < 200 || statusCode >= 300) {
abortWith(new Error(`HTTP error! status: ${statusCode}`));
return;
}
const contentLengthHeader = response.headers['content-length'];
const contentLength = Array.isArray(contentLengthHeader) ? contentLengthHeader[0] : contentLengthHeader;
if (maxSize > 0 && contentLength) {
const parsedContentLength = Number.parseInt(contentLength, 10);
if (!Number.isNaN(parsedContentLength) && parsedContentLength > maxSize) {
abortWith(new MaxSizeExceededError(response.headers as Headers));
return;
}
}
const chunks: Uint8Array[] = [];
let dataSize = 0;
response.on('data', (chunk: Buffer) => {
if (settled) {
return;
}
const buffer = Uint8Array.from(chunk);
dataSize += buffer.byteLength;
if (maxSize > 0 && dataSize > maxSize) {
abortWith(new MaxSizeExceededError(response.headers as Headers));
return;
}
chunks.push(buffer);
});
response.on('end', () => {
resolveOnce({
data: Buffer.concat(chunks),
headers: response.headers as Headers,
});
});
response.on('aborted', () => {
rejectOnce(new Error('Response aborted'));
});
response.on('error', (error: Error) => {
rejectOnce(error);
});
});
request.on('error', (error: Error) => {
rejectOnce(error);
});
request.on('abort', () => {
rejectOnce(new Error('Request aborted'));
});
request.end();
});
}