@fedify/vocab-runtime 2.4.0-dev.2169 → 2.4.0-dev.2190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/{docloader-ClQraSWr.d.cts → docloader-C63enuyr.d.cts} +53 -1
- package/dist/{docloader-ClQraSWr.d.ts → docloader-C63enuyr.d.ts} +53 -1
- package/dist/internal/jsonld-cache.d.cts +1 -1
- package/dist/internal/jsonld-cache.d.ts +1 -1
- package/dist/internal/portable-dereference.d.cts +2 -2
- package/dist/internal/portable-dereference.d.ts +2 -2
- package/dist/mod.cjs +205 -16
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +204 -17
- package/dist/{portable-ilkFPcXa.d.cts → portable-Cux3IA20.d.cts} +1 -1
- package/dist/{portable-DRtvOxTb.d.ts → portable-DBA1FS_L.d.ts} +1 -1
- package/dist/tests/{body-DHAC8qhR.cjs → body-CmZdqiw3.cjs} +60 -1
- package/dist/tests/{body-CNwDKYQs.mjs → body-DMNbCBhz.mjs} +55 -2
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +1 -1
- package/dist/tests/decimal.test.mjs +1 -1
- package/dist/tests/{docloader-DWkl3u7X.mjs → docloader-6iodlx7G.mjs} +152 -18
- package/dist/tests/{docloader-DLkb9qAS.cjs → docloader-B0omnjRS.cjs} +163 -17
- package/dist/tests/docloader.test.cjs +265 -2
- package/dist/tests/docloader.test.mjs +266 -3
- package/dist/tests/{request-Dm3ye46t.cjs → request-BU0mCgOL.cjs} +1 -1
- package/dist/tests/{request-Cg8MQByW.mjs → request-CGmX5n4v.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/package.json +1 -1
- package/src/body.ts +57 -0
- package/src/docloader.test.ts +341 -3
- package/src/docloader.ts +205 -5
- package/src/mod.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-
|
|
2
|
-
import { n as MAX_BODY_SIZE, r as
|
|
1
|
+
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-CGmX5n4v.mjs";
|
|
2
|
+
import { i as readBoundedText, n as MAX_BODY_SIZE, r as readBoundedBytes, t as BodyTooLargeError } from "./body-DMNbCBhz.mjs";
|
|
3
3
|
import { t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
|
|
4
4
|
import { t as HttpHeaderLink } from "./link-Cevmc87v.mjs";
|
|
5
5
|
import { t as UrlError, v as validatePublicUrl } from "./url-D7qRF-dF.mjs";
|
|
@@ -13,6 +13,9 @@ const logger = getLogger([
|
|
|
13
13
|
]);
|
|
14
14
|
const DEFAULT_MAX_REDIRECTION = 20;
|
|
15
15
|
const MAX_HTML_SIZE = 1024 * 1024;
|
|
16
|
+
const MAX_ERROR_BODY_SIZE = 1024 * 1024;
|
|
17
|
+
const DEFAULT_TIMEOUT = 1e4;
|
|
18
|
+
const MAX_TIMEOUT = 2147483647;
|
|
16
19
|
function createResponseMetadata(response) {
|
|
17
20
|
return new Response(null, {
|
|
18
21
|
headers: response.headers,
|
|
@@ -20,6 +23,55 @@ function createResponseMetadata(response) {
|
|
|
20
23
|
statusText: response.statusText
|
|
21
24
|
});
|
|
22
25
|
}
|
|
26
|
+
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
|
|
27
|
+
204,
|
|
28
|
+
205,
|
|
29
|
+
304
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Reads the body of an error response while the document loader is still
|
|
33
|
+
* running, so that its timeout and `AbortSignal` also bound the read, and
|
|
34
|
+
* nothing reading {@link FetchError.response} later waits on the network.
|
|
35
|
+
* The body is kept byte for byte, unless it is too large or cannot be read;
|
|
36
|
+
* then only the status and headers are kept. A response whose status
|
|
37
|
+
* the `Response` constructor does not accept (e.g., 999) is kept as a clone
|
|
38
|
+
* whose body has been read in full; if its body is too large or cannot be
|
|
39
|
+
* read, no response is kept at all.
|
|
40
|
+
*/
|
|
41
|
+
async function bufferErrorResponse(response, url, signal) {
|
|
42
|
+
if (response.status < 200 || response.status > 599) {
|
|
43
|
+
const clone = response.clone();
|
|
44
|
+
try {
|
|
45
|
+
await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
await clone.body?.cancel().catch(() => {});
|
|
48
|
+
if (signal?.aborted) throw error;
|
|
49
|
+
logger.debug("Failed to read the error response body from {url}: {error}", {
|
|
50
|
+
url,
|
|
51
|
+
error
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return clone;
|
|
56
|
+
}
|
|
57
|
+
if (response.body == null || NULL_BODY_STATUSES.has(response.status)) return createResponseMetadata(response);
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (signal?.aborted) throw error;
|
|
63
|
+
logger.debug("Failed to read the error response body from {url}: {error}", {
|
|
64
|
+
url,
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
return createResponseMetadata(response);
|
|
68
|
+
}
|
|
69
|
+
return new Response(body, {
|
|
70
|
+
headers: response.headers,
|
|
71
|
+
status: response.status,
|
|
72
|
+
statusText: response.statusText
|
|
73
|
+
});
|
|
74
|
+
}
|
|
23
75
|
/**
|
|
24
76
|
* Gets a {@link RemoteDocument} from the given response.
|
|
25
77
|
* @param url The URL of the document to load.
|
|
@@ -44,7 +96,7 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
44
96
|
url: documentUrl,
|
|
45
97
|
headers: Object.fromEntries(response.headers.entries())
|
|
46
98
|
});
|
|
47
|
-
throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, response
|
|
99
|
+
throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, await bufferErrorResponse(response, documentUrl, options?.signal));
|
|
48
100
|
}
|
|
49
101
|
const contentType = response.headers.get("Content-Type");
|
|
50
102
|
const jsonLd = contentType == null || contentType === "application/activity+json" || contentType.startsWith("application/activity+json;") || contentType === "application/ld+json" || contentType.startsWith("application/ld+json;");
|
|
@@ -131,9 +183,87 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
131
183
|
};
|
|
132
184
|
}
|
|
133
185
|
/**
|
|
186
|
+
* Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
|
|
187
|
+
* @param timeout The timeout option. `undefined` means the default timeout,
|
|
188
|
+
* and `null` means no timeout.
|
|
189
|
+
* @returns The timeout in milliseconds, or `null` if it is turned off.
|
|
190
|
+
* @throws {RangeError} If the timeout is invalid.
|
|
191
|
+
* @internal
|
|
192
|
+
*/
|
|
193
|
+
function resolveDocumentLoaderTimeout(timeout) {
|
|
194
|
+
if (timeout === void 0) return DEFAULT_TIMEOUT;
|
|
195
|
+
if (timeout === null) return null;
|
|
196
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new RangeError(`The document loader timeout must be a positive finite number of milliseconds, but got ${String(timeout)}.`);
|
|
197
|
+
const ms = Math.ceil(timeout);
|
|
198
|
+
if (ms > MAX_TIMEOUT) throw new RangeError(`The document loader timeout must not be greater than ${MAX_TIMEOUT} milliseconds, but got ${timeout}.`);
|
|
199
|
+
return ms;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Bounds each call of the given document loader by the given timeout.
|
|
203
|
+
* The timeout is combined with the caller's `signal`, and the combined
|
|
204
|
+
* signal is passed to the loader. The call settles no later than the
|
|
205
|
+
* timeout even if the loader is stuck in a step that cannot be aborted,
|
|
206
|
+
* e.g., a DNS lookup.
|
|
207
|
+
*
|
|
208
|
+
* A timed-out call throws a {@link FetchError} without a response, whose
|
|
209
|
+
* `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
|
|
210
|
+
* signal is aborted, its reason is thrown instead.
|
|
211
|
+
* @param loader The document loader to bound.
|
|
212
|
+
* @param timeout The timeout in milliseconds, or `null` for no timeout.
|
|
213
|
+
* It is assumed to have been resolved by
|
|
214
|
+
* {@link resolveDocumentLoaderTimeout}.
|
|
215
|
+
* @returns The bounded document loader.
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
function withDocumentLoaderTimeout(loader, timeout) {
|
|
219
|
+
if (timeout == null) return loader;
|
|
220
|
+
return async (url, options) => {
|
|
221
|
+
const callerSignal = options?.signal;
|
|
222
|
+
callerSignal?.throwIfAborted();
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timeoutReason = new DOMException(`The document loader timed out after ${timeout} ms.`, "TimeoutError");
|
|
225
|
+
let timedOut = false;
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
timedOut = true;
|
|
228
|
+
controller.abort(timeoutReason);
|
|
229
|
+
}, timeout);
|
|
230
|
+
const onCallerAbort = () => controller.abort(callerSignal?.reason);
|
|
231
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
232
|
+
let onAbort;
|
|
233
|
+
const aborted = new Promise((_, reject) => {
|
|
234
|
+
onAbort = () => reject(controller.signal.reason);
|
|
235
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
236
|
+
});
|
|
237
|
+
const loading = loader(url, {
|
|
238
|
+
...options,
|
|
239
|
+
signal: controller.signal
|
|
240
|
+
});
|
|
241
|
+
loading.catch(() => {});
|
|
242
|
+
try {
|
|
243
|
+
return await Promise.race([loading, aborted]);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (callerSignal?.aborted) throw callerSignal.reason;
|
|
246
|
+
if (!timedOut) throw error;
|
|
247
|
+
logger[options?.suppressError ? "warn" : "error"]("Timed out after {timeout} ms while fetching document: {url}", {
|
|
248
|
+
timeout,
|
|
249
|
+
url
|
|
250
|
+
});
|
|
251
|
+
const fetchError = new FetchError(url, `Timed out after ${timeout} ms`);
|
|
252
|
+
fetchError.cause = timeoutReason;
|
|
253
|
+
throw fetchError;
|
|
254
|
+
} finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
257
|
+
if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
134
262
|
* Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
|
|
135
263
|
* At most 20 HTTP redirects and alternate document links are followed in total
|
|
136
264
|
* per call. Revisiting a URL within that chain throws a {@link FetchError}.
|
|
265
|
+
* Each call times out after 10 seconds by default; see
|
|
266
|
+
* {@link DocumentLoaderFactoryOptions.timeout}.
|
|
137
267
|
*
|
|
138
268
|
* The created loader preloads the below frequently used contexts by default
|
|
139
269
|
* (unless `options.skipPreloadedContexts` is set to `true`):
|
|
@@ -153,7 +283,8 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
153
283
|
* @returns The document loader.
|
|
154
284
|
* @since 1.3.0
|
|
155
285
|
*/
|
|
156
|
-
function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
|
|
286
|
+
function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, timeout, userAgent } = {}) {
|
|
287
|
+
const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
|
|
157
288
|
const tracer = trace.getTracerProvider().getTracer(name, version);
|
|
158
289
|
const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
159
290
|
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -167,18 +298,21 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
|
|
|
167
298
|
documentUrl: currentUrl
|
|
168
299
|
};
|
|
169
300
|
}
|
|
170
|
-
if (!allowPrivateAddress)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
url
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
301
|
+
if (!allowPrivateAddress) {
|
|
302
|
+
try {
|
|
303
|
+
await validatePublicUrl(currentUrl);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
306
|
+
url: currentUrl,
|
|
307
|
+
error
|
|
308
|
+
});
|
|
309
|
+
else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
|
|
310
|
+
url: currentUrl,
|
|
311
|
+
error
|
|
312
|
+
});
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
options?.signal?.throwIfAborted();
|
|
182
316
|
}
|
|
183
317
|
visited.add(currentUrl);
|
|
184
318
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
@@ -233,7 +367,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
|
|
|
233
367
|
}
|
|
234
368
|
});
|
|
235
369
|
}
|
|
236
|
-
return (url, options) => load(url, options);
|
|
370
|
+
return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
|
|
237
371
|
}
|
|
238
372
|
//#endregion
|
|
239
|
-
export { getRemoteDocument as n, getDocumentLoader as t };
|
|
373
|
+
export { withDocumentLoaderTimeout as i, getRemoteDocument as n, resolveDocumentLoaderTimeout as r, getDocumentLoader as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const require_request = require("./request-
|
|
2
|
-
const require_body = require("./body-
|
|
1
|
+
const require_request = require("./request-BU0mCgOL.cjs");
|
|
2
|
+
const require_body = require("./body-CmZdqiw3.cjs");
|
|
3
3
|
const require_contexts = require("./contexts-DizzBjz4.cjs");
|
|
4
4
|
const require_link = require("./link-DlKm8bEr.cjs");
|
|
5
5
|
const require_url = require("./url-gLKqAO1H.cjs");
|
|
@@ -13,6 +13,9 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
13
13
|
]);
|
|
14
14
|
const DEFAULT_MAX_REDIRECTION = 20;
|
|
15
15
|
const MAX_HTML_SIZE = 1024 * 1024;
|
|
16
|
+
const MAX_ERROR_BODY_SIZE = 1024 * 1024;
|
|
17
|
+
const DEFAULT_TIMEOUT = 1e4;
|
|
18
|
+
const MAX_TIMEOUT = 2147483647;
|
|
16
19
|
function createResponseMetadata(response) {
|
|
17
20
|
return new Response(null, {
|
|
18
21
|
headers: response.headers,
|
|
@@ -20,6 +23,55 @@ function createResponseMetadata(response) {
|
|
|
20
23
|
statusText: response.statusText
|
|
21
24
|
});
|
|
22
25
|
}
|
|
26
|
+
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
|
|
27
|
+
204,
|
|
28
|
+
205,
|
|
29
|
+
304
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Reads the body of an error response while the document loader is still
|
|
33
|
+
* running, so that its timeout and `AbortSignal` also bound the read, and
|
|
34
|
+
* nothing reading {@link FetchError.response} later waits on the network.
|
|
35
|
+
* The body is kept byte for byte, unless it is too large or cannot be read;
|
|
36
|
+
* then only the status and headers are kept. A response whose status
|
|
37
|
+
* the `Response` constructor does not accept (e.g., 999) is kept as a clone
|
|
38
|
+
* whose body has been read in full; if its body is too large or cannot be
|
|
39
|
+
* read, no response is kept at all.
|
|
40
|
+
*/
|
|
41
|
+
async function bufferErrorResponse(response, url, signal) {
|
|
42
|
+
if (response.status < 200 || response.status > 599) {
|
|
43
|
+
const clone = response.clone();
|
|
44
|
+
try {
|
|
45
|
+
await require_body.readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
await clone.body?.cancel().catch(() => {});
|
|
48
|
+
if (signal?.aborted) throw error;
|
|
49
|
+
logger.debug("Failed to read the error response body from {url}: {error}", {
|
|
50
|
+
url,
|
|
51
|
+
error
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return clone;
|
|
56
|
+
}
|
|
57
|
+
if (response.body == null || NULL_BODY_STATUSES.has(response.status)) return createResponseMetadata(response);
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = await require_body.readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (signal?.aborted) throw error;
|
|
63
|
+
logger.debug("Failed to read the error response body from {url}: {error}", {
|
|
64
|
+
url,
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
return createResponseMetadata(response);
|
|
68
|
+
}
|
|
69
|
+
return new Response(body, {
|
|
70
|
+
headers: response.headers,
|
|
71
|
+
status: response.status,
|
|
72
|
+
statusText: response.statusText
|
|
73
|
+
});
|
|
74
|
+
}
|
|
23
75
|
/**
|
|
24
76
|
* Gets a {@link RemoteDocument} from the given response.
|
|
25
77
|
* @param url The URL of the document to load.
|
|
@@ -44,7 +96,7 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
44
96
|
url: documentUrl,
|
|
45
97
|
headers: Object.fromEntries(response.headers.entries())
|
|
46
98
|
});
|
|
47
|
-
throw new require_request.FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, response
|
|
99
|
+
throw new require_request.FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, await bufferErrorResponse(response, documentUrl, options?.signal));
|
|
48
100
|
}
|
|
49
101
|
const contentType = response.headers.get("Content-Type");
|
|
50
102
|
const jsonLd = contentType == null || contentType === "application/activity+json" || contentType.startsWith("application/activity+json;") || contentType === "application/ld+json" || contentType.startsWith("application/ld+json;");
|
|
@@ -131,9 +183,87 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
131
183
|
};
|
|
132
184
|
}
|
|
133
185
|
/**
|
|
186
|
+
* Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
|
|
187
|
+
* @param timeout The timeout option. `undefined` means the default timeout,
|
|
188
|
+
* and `null` means no timeout.
|
|
189
|
+
* @returns The timeout in milliseconds, or `null` if it is turned off.
|
|
190
|
+
* @throws {RangeError} If the timeout is invalid.
|
|
191
|
+
* @internal
|
|
192
|
+
*/
|
|
193
|
+
function resolveDocumentLoaderTimeout(timeout) {
|
|
194
|
+
if (timeout === void 0) return DEFAULT_TIMEOUT;
|
|
195
|
+
if (timeout === null) return null;
|
|
196
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new RangeError(`The document loader timeout must be a positive finite number of milliseconds, but got ${String(timeout)}.`);
|
|
197
|
+
const ms = Math.ceil(timeout);
|
|
198
|
+
if (ms > MAX_TIMEOUT) throw new RangeError(`The document loader timeout must not be greater than ${MAX_TIMEOUT} milliseconds, but got ${timeout}.`);
|
|
199
|
+
return ms;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Bounds each call of the given document loader by the given timeout.
|
|
203
|
+
* The timeout is combined with the caller's `signal`, and the combined
|
|
204
|
+
* signal is passed to the loader. The call settles no later than the
|
|
205
|
+
* timeout even if the loader is stuck in a step that cannot be aborted,
|
|
206
|
+
* e.g., a DNS lookup.
|
|
207
|
+
*
|
|
208
|
+
* A timed-out call throws a {@link FetchError} without a response, whose
|
|
209
|
+
* `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
|
|
210
|
+
* signal is aborted, its reason is thrown instead.
|
|
211
|
+
* @param loader The document loader to bound.
|
|
212
|
+
* @param timeout The timeout in milliseconds, or `null` for no timeout.
|
|
213
|
+
* It is assumed to have been resolved by
|
|
214
|
+
* {@link resolveDocumentLoaderTimeout}.
|
|
215
|
+
* @returns The bounded document loader.
|
|
216
|
+
* @internal
|
|
217
|
+
*/
|
|
218
|
+
function withDocumentLoaderTimeout(loader, timeout) {
|
|
219
|
+
if (timeout == null) return loader;
|
|
220
|
+
return async (url, options) => {
|
|
221
|
+
const callerSignal = options?.signal;
|
|
222
|
+
callerSignal?.throwIfAborted();
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timeoutReason = new DOMException(`The document loader timed out after ${timeout} ms.`, "TimeoutError");
|
|
225
|
+
let timedOut = false;
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
timedOut = true;
|
|
228
|
+
controller.abort(timeoutReason);
|
|
229
|
+
}, timeout);
|
|
230
|
+
const onCallerAbort = () => controller.abort(callerSignal?.reason);
|
|
231
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
232
|
+
let onAbort;
|
|
233
|
+
const aborted = new Promise((_, reject) => {
|
|
234
|
+
onAbort = () => reject(controller.signal.reason);
|
|
235
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
236
|
+
});
|
|
237
|
+
const loading = loader(url, {
|
|
238
|
+
...options,
|
|
239
|
+
signal: controller.signal
|
|
240
|
+
});
|
|
241
|
+
loading.catch(() => {});
|
|
242
|
+
try {
|
|
243
|
+
return await Promise.race([loading, aborted]);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (callerSignal?.aborted) throw callerSignal.reason;
|
|
246
|
+
if (!timedOut) throw error;
|
|
247
|
+
logger[options?.suppressError ? "warn" : "error"]("Timed out after {timeout} ms while fetching document: {url}", {
|
|
248
|
+
timeout,
|
|
249
|
+
url
|
|
250
|
+
});
|
|
251
|
+
const fetchError = new require_request.FetchError(url, `Timed out after ${timeout} ms`);
|
|
252
|
+
fetchError.cause = timeoutReason;
|
|
253
|
+
throw fetchError;
|
|
254
|
+
} finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
257
|
+
if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
134
262
|
* Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
|
|
135
263
|
* At most 20 HTTP redirects and alternate document links are followed in total
|
|
136
264
|
* per call. Revisiting a URL within that chain throws a {@link FetchError}.
|
|
265
|
+
* Each call times out after 10 seconds by default; see
|
|
266
|
+
* {@link DocumentLoaderFactoryOptions.timeout}.
|
|
137
267
|
*
|
|
138
268
|
* The created loader preloads the below frequently used contexts by default
|
|
139
269
|
* (unless `options.skipPreloadedContexts` is set to `true`):
|
|
@@ -153,7 +283,8 @@ async function getRemoteDocument(url, response, fetch, options) {
|
|
|
153
283
|
* @returns The document loader.
|
|
154
284
|
* @since 1.3.0
|
|
155
285
|
*/
|
|
156
|
-
function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
|
|
286
|
+
function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, timeout, userAgent } = {}) {
|
|
287
|
+
const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
|
|
157
288
|
const tracer = _opentelemetry_api.trace.getTracerProvider().getTracer(require_request.name, require_request.version);
|
|
158
289
|
const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
159
290
|
async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -167,18 +298,21 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
|
|
|
167
298
|
documentUrl: currentUrl
|
|
168
299
|
};
|
|
169
300
|
}
|
|
170
|
-
if (!allowPrivateAddress)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
url
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
301
|
+
if (!allowPrivateAddress) {
|
|
302
|
+
try {
|
|
303
|
+
await require_url.validatePublicUrl(currentUrl);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
306
|
+
url: currentUrl,
|
|
307
|
+
error
|
|
308
|
+
});
|
|
309
|
+
else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
|
|
310
|
+
url: currentUrl,
|
|
311
|
+
error
|
|
312
|
+
});
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
options?.signal?.throwIfAborted();
|
|
182
316
|
}
|
|
183
317
|
visited.add(currentUrl);
|
|
184
318
|
return await tracer.startActiveSpan("activitypub.fetch_document", {
|
|
@@ -233,7 +367,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
|
|
|
233
367
|
}
|
|
234
368
|
});
|
|
235
369
|
}
|
|
236
|
-
return (url, options) => load(url, options);
|
|
370
|
+
return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
|
|
237
371
|
}
|
|
238
372
|
//#endregion
|
|
239
373
|
Object.defineProperty(exports, "getDocumentLoader", {
|
|
@@ -248,3 +382,15 @@ Object.defineProperty(exports, "getRemoteDocument", {
|
|
|
248
382
|
return getRemoteDocument;
|
|
249
383
|
}
|
|
250
384
|
});
|
|
385
|
+
Object.defineProperty(exports, "resolveDocumentLoaderTimeout", {
|
|
386
|
+
enumerable: true,
|
|
387
|
+
get: function() {
|
|
388
|
+
return resolveDocumentLoaderTimeout;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
Object.defineProperty(exports, "withDocumentLoaderTimeout", {
|
|
392
|
+
enumerable: true,
|
|
393
|
+
get: function() {
|
|
394
|
+
return withDocumentLoaderTimeout;
|
|
395
|
+
}
|
|
396
|
+
});
|