@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.4.1
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 +4 -2
- package/dist/{tests/docloader-Ck8bcBir.mjs → contexts-B09jb9Pi.cjs} +173 -274
- package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DWrj4AGR.js} +169 -293
- package/dist/{docloader-C_dir7Xb.d.ts → docloader-BMHljyYb.d.cts} +68 -2
- package/dist/{docloader-D2DTRiyA.d.cts → docloader-BMHljyYb.d.ts} +68 -2
- package/dist/internal/jsonld-cache.cjs +70 -1
- package/dist/internal/jsonld-cache.d.cts +35 -2
- package/dist/internal/jsonld-cache.d.ts +35 -2
- package/dist/internal/jsonld-cache.js +67 -2
- package/dist/internal/portable-dereference.cjs +721 -0
- package/dist/internal/portable-dereference.d.cts +349 -0
- package/dist/internal/portable-dereference.d.ts +349 -0
- package/dist/internal/portable-dereference.js +702 -0
- package/dist/internal/signed-representation.cjs +378 -0
- package/dist/internal/signed-representation.d.cts +144 -0
- package/dist/internal/signed-representation.d.ts +144 -0
- package/dist/internal/signed-representation.js +371 -0
- package/dist/jsonld.cjs +2 -2
- package/dist/mod.cjs +762 -4529
- package/dist/mod.d.cts +427 -9
- package/dist/mod.d.ts +427 -10
- package/dist/mod.js +742 -4524
- package/dist/portable-6Aj-7dZJ.d.cts +160 -0
- package/dist/portable-nkTiqH0w.d.ts +160 -0
- package/dist/tests/body-DpC9p-iG.mjs +130 -0
- package/dist/tests/body-cKLBprYm.cjs +153 -0
- package/dist/tests/body.test.cjs +89 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/contexts-C9Lj11SC.cjs +4533 -0
- package/dist/tests/contexts-duRA_dDC.mjs +4522 -0
- package/dist/tests/decimal.test.cjs +9 -7
- package/dist/tests/decimal.test.mjs +9 -6
- package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
- package/dist/tests/digest-COC7xDiQ.mjs +141 -0
- package/dist/tests/digest.test.cjs +102 -0
- package/dist/tests/digest.test.d.cts +1 -0
- package/dist/tests/digest.test.d.mts +1 -0
- package/dist/tests/digest.test.mjs +103 -0
- package/dist/tests/docloader-DyZ1JGIl.cjs +398 -0
- package/dist/tests/docloader-tUmMeXjU.mjs +375 -0
- package/dist/tests/docloader.test.cjs +816 -39
- package/dist/tests/docloader.test.mjs +810 -34
- package/dist/tests/internal/multicodec.test.cjs +2 -3
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/internal/portable-dereference.test.cjs +172 -0
- package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
- package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
- package/dist/tests/internal/portable-dereference.test.mjs +173 -0
- package/dist/tests/jsonld-CmDhHNTx.cjs +12 -0
- package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
- package/dist/tests/jsonld-cache-BEXiiM-V.cjs +386 -0
- package/dist/tests/jsonld-cache-DFnWsLuZ.mjs +339 -0
- package/dist/tests/jsonld-cache.test.cjs +102 -298
- package/dist/tests/jsonld-cache.test.mjs +95 -289
- package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
- package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
- package/dist/tests/key.test.cjs +6 -5
- package/dist/tests/key.test.mjs +6 -4
- package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DYRmJqFn.mjs} +5 -2
- package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-_PqsL1ae.cjs} +5 -2
- package/dist/tests/langstr.test.cjs +11 -4
- package/dist/tests/langstr.test.mjs +9 -2
- package/dist/tests/langtag-CUppHs_s.mjs +45 -0
- package/dist/tests/langtag-Czjvxfzx.cjs +50 -0
- package/dist/tests/langtag.test.cjs +45 -0
- package/dist/tests/langtag.test.d.cts +1 -0
- package/dist/tests/langtag.test.d.mts +1 -0
- package/dist/tests/langtag.test.mjs +46 -0
- package/dist/tests/link.test.cjs +2 -3
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +8 -9
- package/dist/tests/multibase/multibase.test.mjs +6 -6
- package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
- package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
- package/dist/tests/portable-dereference-DmTYDy2p.mjs +250 -0
- package/dist/tests/portable-dereference-qTLMqe2_.cjs +279 -0
- package/dist/tests/portable-media-5p8azal0.cjs +153 -0
- package/dist/tests/portable-media-B_lt9MMA.mjs +148 -0
- package/dist/tests/portable-media.test.cjs +222 -0
- package/dist/tests/portable-media.test.d.cts +1 -0
- package/dist/tests/portable-media.test.d.mts +1 -0
- package/dist/tests/portable-media.test.mjs +223 -0
- package/dist/tests/portable-workers.test.cjs +36 -0
- package/dist/tests/portable-workers.test.d.cts +2 -0
- package/dist/tests/portable-workers.test.d.mts +2 -0
- package/dist/tests/portable-workers.test.mjs +35 -0
- package/dist/tests/{request-uk51rkhO.cjs → request-BgjA9wyl.cjs} +10 -4
- package/dist/tests/{request-C8CaGwtt.mjs → request-DodZNukP.mjs} +8 -2
- package/dist/tests/request.test.cjs +7 -4
- package/dist/tests/request.test.mjs +5 -2
- package/dist/tests/signed-representation.test.cjs +588 -0
- package/dist/tests/signed-representation.test.d.cts +1 -0
- package/dist/tests/signed-representation.test.d.mts +1 -0
- package/dist/tests/signed-representation.test.mjs +589 -0
- package/dist/tests/temporal.test.cjs +1 -2
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url-BNakuZ8k.cjs +998 -0
- package/dist/tests/url-DMxmp7ZG.mjs +859 -0
- package/dist/tests/url.test.cjs +489 -6
- package/dist/tests/url.test.mjs +489 -5
- package/dist/url-DrGTR8yv.cjs +993 -0
- package/dist/url-Dzyp-NsC.js +860 -0
- package/package.json +27 -4
- package/scripts/test-bun.mjs +17 -0
- package/src/body.test.ts +125 -0
- package/src/body.ts +152 -0
- package/src/contexts/cid-v1.json +114 -0
- package/src/contexts/fep-22cd.json +21 -0
- package/src/contexts/fep-6757.json +12 -0
- package/src/contexts/joinmastodon.json +1 -1
- package/src/contexts/miscellany.json +17 -0
- package/src/contexts.ts +47 -1
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.test.ts +1066 -27
- package/src/docloader.ts +322 -105
- package/src/internal/jsonld-cache.ts +97 -1
- package/src/internal/portable-dereference.test.ts +254 -0
- package/src/internal/portable-dereference.ts +1000 -0
- package/src/internal/signed-representation.ts +565 -0
- package/src/jsonld-cache.test.ts +89 -0
- package/src/key.test.ts +9 -0
- package/src/key.ts +11 -1
- package/src/langstr.test.ts +18 -1
- package/src/langstr.ts +6 -2
- package/src/langtag.test.ts +53 -0
- package/src/langtag.ts +76 -0
- package/src/mod.ts +29 -0
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-media.test.ts +293 -0
- package/src/portable-media.ts +264 -0
- package/src/portable-workers.test.ts +81 -0
- package/src/portable.ts +180 -0
- package/src/preprocessor.ts +7 -0
- package/src/request.test.ts +10 -1
- package/src/request.ts +9 -1
- package/src/signed-representation.test.ts +338 -0
- package/src/url.test.ts +823 -3
- package/src/url.ts +700 -23
- package/tsdown.config.ts +2 -0
- package/dist/tests/url-CsOV_B_P.cjs +0 -482
- package/dist/tests/url-Du7RQQgP.mjs +0 -392
- package/dist/url-DD4F0ULf.cjs +0 -483
- package/dist/url-DGVbSVVi.js +0 -393
- /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
- /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { t as FetchError } from "./request-DodZNukP.mjs";
|
|
2
|
+
import { n as cid_v1_default, t as preloadedContexts } from "./contexts-duRA_dDC.mjs";
|
|
3
|
+
import { n as UrlError } from "./url-DMxmp7ZG.mjs";
|
|
4
|
+
import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-tUmMeXjU.mjs";
|
|
5
|
+
import { t as jsonld_default } from "./jsonld-DweIJ07b.mjs";
|
|
6
|
+
import { gzipSync } from "node:zlib";
|
|
7
|
+
import { configure, reset } from "@logtape/logtape";
|
|
8
|
+
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
9
|
+
import dns from "node:dns/promises";
|
|
10
|
+
import { test } from "@fedify/fixture";
|
|
11
|
+
import { createServer } from "node:http";
|
|
6
12
|
//#region \0rolldown/runtime.js
|
|
7
13
|
var __create = Object.create;
|
|
8
14
|
var __defProp = Object.defineProperty;
|
|
@@ -130,27 +136,27 @@ function parse(input, loose) {
|
|
|
130
136
|
}
|
|
131
137
|
//#endregion
|
|
132
138
|
//#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/TypeDescriptor.js
|
|
133
|
-
const valueTypes = new Set([
|
|
139
|
+
const valueTypes = /* @__PURE__ */ new Set([
|
|
134
140
|
"boolean",
|
|
135
141
|
"number",
|
|
136
142
|
"null",
|
|
137
143
|
"string",
|
|
138
144
|
"undefined"
|
|
139
145
|
]);
|
|
140
|
-
const referenceTypes = new Set([
|
|
146
|
+
const referenceTypes = /* @__PURE__ */ new Set([
|
|
141
147
|
"array",
|
|
142
148
|
"function",
|
|
143
149
|
"object",
|
|
144
150
|
"symbol"
|
|
145
151
|
]);
|
|
146
|
-
const detectableTypes = new Set([
|
|
152
|
+
const detectableTypes = /* @__PURE__ */ new Set([
|
|
147
153
|
"boolean",
|
|
148
154
|
"function",
|
|
149
155
|
"number",
|
|
150
156
|
"string",
|
|
151
157
|
"symbol"
|
|
152
158
|
]);
|
|
153
|
-
const typeConstructors = new Set([
|
|
159
|
+
const typeConstructors = /* @__PURE__ */ new Set([
|
|
154
160
|
Boolean,
|
|
155
161
|
Number,
|
|
156
162
|
String
|
|
@@ -221,7 +227,7 @@ var TypeDescriptor = class TypeDescriptor {
|
|
|
221
227
|
};
|
|
222
228
|
//#endregion
|
|
223
229
|
//#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/IsSubsetOf.js
|
|
224
|
-
const allowedTypes = new Set([
|
|
230
|
+
const allowedTypes = /* @__PURE__ */ new Set([
|
|
225
231
|
"array",
|
|
226
232
|
"object",
|
|
227
233
|
"function",
|
|
@@ -713,7 +719,14 @@ var __classPrivateFieldGet = function(receiver, state, kind, f) {
|
|
|
713
719
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
714
720
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
715
721
|
};
|
|
716
|
-
var _Route_instances
|
|
722
|
+
var _Route_instances;
|
|
723
|
+
var _a;
|
|
724
|
+
var _Route_responseSubscriptions;
|
|
725
|
+
var _Route_validate;
|
|
726
|
+
var _Route_sanitize;
|
|
727
|
+
var _Route_generateMatcher;
|
|
728
|
+
var _Route_limit;
|
|
729
|
+
var _Route_delayResponse;
|
|
717
730
|
var RouteConfigWrapper = class {
|
|
718
731
|
constructor(config) {
|
|
719
732
|
Object.assign(this, config);
|
|
@@ -1225,6 +1238,52 @@ var esm_default = new class FetchMock {
|
|
|
1225
1238
|
}({ ...defaultFetchMockConfig });
|
|
1226
1239
|
//#endregion
|
|
1227
1240
|
//#region src/docloader.test.ts
|
|
1241
|
+
test("getRemoteDocument() forwards options to alternate documents", async () => {
|
|
1242
|
+
const records = [];
|
|
1243
|
+
await reset();
|
|
1244
|
+
await configure({
|
|
1245
|
+
sinks: { capture: (record) => records.push(record) },
|
|
1246
|
+
loggers: [{
|
|
1247
|
+
category: ["fedify"],
|
|
1248
|
+
lowestLevel: "debug",
|
|
1249
|
+
sinks: ["capture"]
|
|
1250
|
+
}]
|
|
1251
|
+
});
|
|
1252
|
+
try {
|
|
1253
|
+
for (const linkHeader of [true, false]) for (const suppressError of [
|
|
1254
|
+
true,
|
|
1255
|
+
false,
|
|
1256
|
+
void 0
|
|
1257
|
+
]) {
|
|
1258
|
+
records.length = 0;
|
|
1259
|
+
const url = "https://example.com/alternate-source";
|
|
1260
|
+
const alternateUrl = "https://example.com/missing-alternate";
|
|
1261
|
+
const options = {
|
|
1262
|
+
suppressError,
|
|
1263
|
+
signal: new AbortController().signal
|
|
1264
|
+
};
|
|
1265
|
+
const response = new Response(`<link rel="alternate" type="application/activity+json" href="${alternateUrl}">`, { headers: {
|
|
1266
|
+
"Content-Type": "text/html",
|
|
1267
|
+
...linkHeader ? { Link: `<${alternateUrl}>; rel="alternate"; type="application/activity+json"` } : {}
|
|
1268
|
+
} });
|
|
1269
|
+
let alternateRequests = 0;
|
|
1270
|
+
await rejects(() => getRemoteDocument(url, response, async (requestedUrl, forwarded) => {
|
|
1271
|
+
alternateRequests++;
|
|
1272
|
+
deepStrictEqual(requestedUrl, alternateUrl);
|
|
1273
|
+
deepStrictEqual(forwarded, options);
|
|
1274
|
+
return await getRemoteDocument(requestedUrl, new Response(null, { status: 404 }), () => {
|
|
1275
|
+
throw new Error("Unexpected additional alternate");
|
|
1276
|
+
}, forwarded);
|
|
1277
|
+
}, options), FetchError);
|
|
1278
|
+
deepStrictEqual(alternateRequests, 1);
|
|
1279
|
+
const failures = records.filter((record) => record.rawMessage === "Failed to fetch document: {status} {url} {headers}");
|
|
1280
|
+
deepStrictEqual(failures.length, 1);
|
|
1281
|
+
deepStrictEqual(failures[0].level, suppressError ? "warning" : "error");
|
|
1282
|
+
}
|
|
1283
|
+
} finally {
|
|
1284
|
+
await reset();
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1228
1287
|
test("new FetchError()", () => {
|
|
1229
1288
|
const e = new FetchError("https://example.com/", "An error message.");
|
|
1230
1289
|
deepStrictEqual(e.name, "FetchError");
|
|
@@ -1247,7 +1306,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1247
1306
|
name: "Fetched object",
|
|
1248
1307
|
type: "Object"
|
|
1249
1308
|
} });
|
|
1250
|
-
await t.
|
|
1309
|
+
await t.step("ok", async () => {
|
|
1251
1310
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
|
|
1252
1311
|
contextUrl: null,
|
|
1253
1312
|
documentUrl: "https://example.com/object",
|
|
@@ -1290,7 +1349,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1290
1349
|
Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
|
|
1291
1350
|
}
|
|
1292
1351
|
});
|
|
1293
|
-
await t.
|
|
1352
|
+
await t.step("Link header", async () => {
|
|
1294
1353
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1295
1354
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1296
1355
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1311,7 +1370,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1311
1370
|
}
|
|
1312
1371
|
});
|
|
1313
1372
|
});
|
|
1314
|
-
await t.
|
|
1373
|
+
await t.step("Link header relative url", async () => {
|
|
1315
1374
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1316
1375
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1317
1376
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1332,7 +1391,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1332
1391
|
}
|
|
1333
1392
|
});
|
|
1334
1393
|
});
|
|
1335
|
-
await t.
|
|
1394
|
+
await t.step("wrong Link header syntax", async () => {
|
|
1336
1395
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
|
|
1337
1396
|
contextUrl: null,
|
|
1338
1397
|
documentUrl: "https://example.com/obj-w-wrong-link",
|
|
@@ -1356,7 +1415,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1356
1415
|
</html>`,
|
|
1357
1416
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1358
1417
|
});
|
|
1359
|
-
await t.
|
|
1418
|
+
await t.step("HTML <link>", async () => {
|
|
1360
1419
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-link"), {
|
|
1361
1420
|
contextUrl: null,
|
|
1362
1421
|
documentUrl: "https://example.com/object",
|
|
@@ -1380,7 +1439,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1380
1439
|
</html>`,
|
|
1381
1440
|
headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" }
|
|
1382
1441
|
});
|
|
1383
|
-
await t.
|
|
1442
|
+
await t.step("XHTML <link>", async () => {
|
|
1384
1443
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/xhtml-link"), {
|
|
1385
1444
|
contextUrl: null,
|
|
1386
1445
|
documentUrl: "https://example.com/object",
|
|
@@ -1406,7 +1465,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1406
1465
|
</html>`,
|
|
1407
1466
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1408
1467
|
});
|
|
1409
|
-
await t.
|
|
1468
|
+
await t.step("HTML <a>", async () => {
|
|
1410
1469
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
|
|
1411
1470
|
contextUrl: null,
|
|
1412
1471
|
documentUrl: "https://example.com/object",
|
|
@@ -1428,7 +1487,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1428
1487
|
</html>`,
|
|
1429
1488
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1430
1489
|
});
|
|
1431
|
-
await t.
|
|
1490
|
+
await t.step("HTML without ActivityPub alternate link", async () => {
|
|
1432
1491
|
await rejects(() => fetchDocumentLoader("https://example.com/html-no-alternate"), (error) => {
|
|
1433
1492
|
ok(error instanceof FetchError);
|
|
1434
1493
|
ok(error.message.includes("HTML document has no ActivityPub alternate link"));
|
|
@@ -1448,7 +1507,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1448
1507
|
},
|
|
1449
1508
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1450
1509
|
});
|
|
1451
|
-
await t.
|
|
1510
|
+
await t.step("wrong Content-Type with JSON body", async () => {
|
|
1452
1511
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
|
|
1453
1512
|
contextUrl: null,
|
|
1454
1513
|
documentUrl: "https://example.com/wrong-content-type",
|
|
@@ -1467,7 +1526,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1467
1526
|
"Content-Type": "text/html; charset=utf-8"
|
|
1468
1527
|
}
|
|
1469
1528
|
});
|
|
1470
|
-
await t.
|
|
1529
|
+
await t.step("HTML Content-Length over limit", async () => {
|
|
1471
1530
|
await rejects(() => fetchDocumentLoader("https://example.com/large-html"), (error) => {
|
|
1472
1531
|
ok(error instanceof FetchError);
|
|
1473
1532
|
ok(error.message.includes("HTML document is too large to scan for an ActivityPub alternate link"));
|
|
@@ -1477,32 +1536,211 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1477
1536
|
return true;
|
|
1478
1537
|
});
|
|
1479
1538
|
});
|
|
1480
|
-
await t.
|
|
1539
|
+
await t.step("HTML Content-Length over limit cancels body", async () => {
|
|
1481
1540
|
let canceled = false;
|
|
1482
1541
|
const response = new Response("<!DOCTYPE html>", { headers: {
|
|
1483
1542
|
"Content-Length": String(1048577),
|
|
1484
1543
|
"Content-Type": "text/html; charset=utf-8"
|
|
1485
1544
|
} });
|
|
1486
|
-
Object.defineProperty(response, "body", { value: { cancel
|
|
1545
|
+
Object.defineProperty(response, "body", { value: new ReadableStream({ cancel() {
|
|
1487
1546
|
canceled = true;
|
|
1488
|
-
} } });
|
|
1547
|
+
} }) });
|
|
1489
1548
|
await rejects(() => getRemoteDocument("https://example.com/large-html-cancel", response, () => {
|
|
1490
1549
|
throw new Error("unexpected alternate fetch");
|
|
1491
1550
|
}), FetchError);
|
|
1492
1551
|
deepStrictEqual(canceled, true);
|
|
1493
1552
|
});
|
|
1494
1553
|
esm_default.get("https://example.com/404", { status: 404 });
|
|
1495
|
-
await t.
|
|
1554
|
+
await t.step("not ok", async () => {
|
|
1496
1555
|
await rejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
|
|
1497
1556
|
});
|
|
1498
|
-
await t.
|
|
1557
|
+
await t.step("preloaded contexts", async () => {
|
|
1499
1558
|
for (const [url, document] of Object.entries(preloadedContexts)) deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1500
1559
|
contextUrl: null,
|
|
1501
1560
|
documentUrl: url,
|
|
1502
1561
|
document
|
|
1503
1562
|
});
|
|
1504
1563
|
});
|
|
1505
|
-
await t.
|
|
1564
|
+
await t.step("preloaded Mastodon attribution domains", async () => {
|
|
1565
|
+
const context = ["https://www.w3.org/ns/activitystreams", "http://joinmastodon.org/ns"];
|
|
1566
|
+
const expanded = await jsonld_default.expand({
|
|
1567
|
+
"@context": context,
|
|
1568
|
+
id: "https://social.example/users/alice",
|
|
1569
|
+
type: "Person",
|
|
1570
|
+
attributionDomains: ["blog.example"]
|
|
1571
|
+
}, { documentLoader: fetchDocumentLoader });
|
|
1572
|
+
deepStrictEqual(expanded, [{
|
|
1573
|
+
"@id": "https://social.example/users/alice",
|
|
1574
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Person"],
|
|
1575
|
+
"http://joinmastodon.org/ns#attributionDomains": [{ "@value": "blog.example" }]
|
|
1576
|
+
}]);
|
|
1577
|
+
deepStrictEqual((await jsonld_default.compact(expanded, context, { documentLoader: fetchDocumentLoader })).attributionDomains, ["blog.example"]);
|
|
1578
|
+
});
|
|
1579
|
+
esm_default.get("https://www.w3.org/ns/cid/v1", { status: 503 });
|
|
1580
|
+
await t.step("preloaded CID v1 context", async () => {
|
|
1581
|
+
const url = "https://www.w3.org/ns/cid/v1";
|
|
1582
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1583
|
+
contextUrl: null,
|
|
1584
|
+
documentUrl: url,
|
|
1585
|
+
document: cid_v1_default
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
await t.step("preloaded FEP-ef61 context", async () => {
|
|
1589
|
+
const url = "https://w3id.org/fep/ef61";
|
|
1590
|
+
ok(url in preloadedContexts);
|
|
1591
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1592
|
+
contextUrl: null,
|
|
1593
|
+
documentUrl: url,
|
|
1594
|
+
document: { "@context": {
|
|
1595
|
+
gateways: {
|
|
1596
|
+
"@id": "https://w3id.org/fep/ef61/gateways",
|
|
1597
|
+
"@type": "@id",
|
|
1598
|
+
"@container": "@list"
|
|
1599
|
+
},
|
|
1600
|
+
digestMultibase: "https://www.w3.org/ns/credentials/v2#digestMultibase"
|
|
1601
|
+
} }
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
1605
|
+
await t.step("preloaded FEP-7aa9 context", async () => {
|
|
1606
|
+
const url = "https://w3id.org/fep/7aa9";
|
|
1607
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1608
|
+
contextUrl: null,
|
|
1609
|
+
documentUrl: url,
|
|
1610
|
+
document: { "@context": {
|
|
1611
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
1612
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
1613
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
1614
|
+
"FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
1615
|
+
"topic": {
|
|
1616
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
1617
|
+
"@type": "@id"
|
|
1618
|
+
},
|
|
1619
|
+
"featuredObject": {
|
|
1620
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
1621
|
+
"@type": "@id"
|
|
1622
|
+
},
|
|
1623
|
+
"canFeature": {
|
|
1624
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
1625
|
+
"@type": "@id"
|
|
1626
|
+
},
|
|
1627
|
+
"featureAuthorization": {
|
|
1628
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
1629
|
+
"@type": "@id"
|
|
1630
|
+
}
|
|
1631
|
+
} }
|
|
1632
|
+
});
|
|
1633
|
+
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1634
|
+
});
|
|
1635
|
+
esm_default.get("https://w3id.org/fep/6757", { status: 502 });
|
|
1636
|
+
await t.step("preloaded FEP-6757 context", async () => {
|
|
1637
|
+
const url = "https://w3id.org/fep/6757";
|
|
1638
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1639
|
+
contextUrl: null,
|
|
1640
|
+
documentUrl: url,
|
|
1641
|
+
document: { "@context": {
|
|
1642
|
+
license: {
|
|
1643
|
+
"@id": "http://purl.org/dc/terms/license",
|
|
1644
|
+
"@type": "@id"
|
|
1645
|
+
},
|
|
1646
|
+
preferredLicense: {
|
|
1647
|
+
"@id": "https://w3id.org/fep/6757#preferredLicense",
|
|
1648
|
+
"@type": "@id"
|
|
1649
|
+
}
|
|
1650
|
+
} }
|
|
1651
|
+
});
|
|
1652
|
+
deepStrictEqual(await jsonld_default.expand({
|
|
1653
|
+
"@context": ["https://www.w3.org/ns/activitystreams", url],
|
|
1654
|
+
type: "Person",
|
|
1655
|
+
license: "https://creativecommons.org/licenses/by/4.0/",
|
|
1656
|
+
preferredLicense: "https://creativecommons.org/licenses/by-nc/4.0/"
|
|
1657
|
+
}, { documentLoader: fetchDocumentLoader }), [{
|
|
1658
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Person"],
|
|
1659
|
+
"http://purl.org/dc/terms/license": [{ "@id": "https://creativecommons.org/licenses/by/4.0/" }],
|
|
1660
|
+
"https://w3id.org/fep/6757#preferredLicense": [{ "@id": "https://creativecommons.org/licenses/by-nc/4.0/" }]
|
|
1661
|
+
}]);
|
|
1662
|
+
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1663
|
+
});
|
|
1664
|
+
esm_default.get("https://w3id.org/fep/22cd", { status: 502 });
|
|
1665
|
+
await t.step("preloaded FEP-22cd context", async () => {
|
|
1666
|
+
const url = "https://w3id.org/fep/22cd";
|
|
1667
|
+
const articleId = "https://example.com/articles/1";
|
|
1668
|
+
const translatorId = "https://example.com/users/alice";
|
|
1669
|
+
const revisionId = "https://example.com/articles/1/revisions/1";
|
|
1670
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1671
|
+
contextUrl: null,
|
|
1672
|
+
documentUrl: url,
|
|
1673
|
+
document: { "@context": {
|
|
1674
|
+
"fep-22cd": "https://w3id.org/fep/22cd#",
|
|
1675
|
+
schema: "https://schema.org/",
|
|
1676
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
1677
|
+
translations: {
|
|
1678
|
+
"@id": "fep-22cd:translations",
|
|
1679
|
+
"@container": "@set"
|
|
1680
|
+
},
|
|
1681
|
+
Translation: "fep-22cd:Translation",
|
|
1682
|
+
sourceUpdated: {
|
|
1683
|
+
"@id": "fep-22cd:sourceUpdated",
|
|
1684
|
+
"@type": "xsd:dateTime"
|
|
1685
|
+
},
|
|
1686
|
+
translator: {
|
|
1687
|
+
"@id": "schema:translator",
|
|
1688
|
+
"@type": "@id",
|
|
1689
|
+
"@container": "@set"
|
|
1690
|
+
},
|
|
1691
|
+
inLanguage: "schema:inLanguage",
|
|
1692
|
+
translationOfWork: {
|
|
1693
|
+
"@id": "schema:translationOfWork",
|
|
1694
|
+
"@type": "@id"
|
|
1695
|
+
},
|
|
1696
|
+
isBasedOn: {
|
|
1697
|
+
"@id": "schema:isBasedOn",
|
|
1698
|
+
"@type": "@id"
|
|
1699
|
+
}
|
|
1700
|
+
} }
|
|
1701
|
+
});
|
|
1702
|
+
deepStrictEqual(await jsonld_default.expand({
|
|
1703
|
+
"@context": ["https://www.w3.org/ns/activitystreams", url],
|
|
1704
|
+
id: articleId,
|
|
1705
|
+
type: "Article",
|
|
1706
|
+
contentMap: {
|
|
1707
|
+
en: "Original",
|
|
1708
|
+
ko: "Translation"
|
|
1709
|
+
},
|
|
1710
|
+
translations: [{
|
|
1711
|
+
type: "Translation",
|
|
1712
|
+
inLanguage: "ko",
|
|
1713
|
+
translator: [translatorId],
|
|
1714
|
+
translationOfWork: articleId,
|
|
1715
|
+
sourceUpdated: "2026-09-01T00:00:00Z",
|
|
1716
|
+
isBasedOn: revisionId
|
|
1717
|
+
}]
|
|
1718
|
+
}, { documentLoader: fetchDocumentLoader }), [{
|
|
1719
|
+
"@id": articleId,
|
|
1720
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Article"],
|
|
1721
|
+
"https://www.w3.org/ns/activitystreams#content": [{
|
|
1722
|
+
"@language": "en",
|
|
1723
|
+
"@value": "Original"
|
|
1724
|
+
}, {
|
|
1725
|
+
"@language": "ko",
|
|
1726
|
+
"@value": "Translation"
|
|
1727
|
+
}],
|
|
1728
|
+
"https://w3id.org/fep/22cd#translations": [{
|
|
1729
|
+
"@type": ["https://w3id.org/fep/22cd#Translation"],
|
|
1730
|
+
"https://schema.org/inLanguage": [{ "@value": "ko" }],
|
|
1731
|
+
"https://schema.org/translator": [{ "@id": translatorId }],
|
|
1732
|
+
"https://schema.org/translationOfWork": [{ "@id": articleId }],
|
|
1733
|
+
"https://w3id.org/fep/22cd#sourceUpdated": [{
|
|
1734
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
|
|
1735
|
+
"@value": "2026-09-01T00:00:00Z"
|
|
1736
|
+
}],
|
|
1737
|
+
"https://schema.org/isBasedOn": [{ "@id": revisionId }]
|
|
1738
|
+
}]
|
|
1739
|
+
}]);
|
|
1740
|
+
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1741
|
+
deepStrictEqual(esm_default.callHistory.calls("https://www.w3.org/ns/activitystreams").length, 0);
|
|
1742
|
+
});
|
|
1743
|
+
await t.step("deny non-HTTP/HTTPS", async () => {
|
|
1506
1744
|
await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
|
|
1507
1745
|
});
|
|
1508
1746
|
esm_default.get("https://example.com/localhost-redirect", {
|
|
@@ -1527,13 +1765,13 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1527
1765
|
name: "Fetched object",
|
|
1528
1766
|
type: "Object"
|
|
1529
1767
|
} });
|
|
1530
|
-
await t.
|
|
1768
|
+
await t.step("allowPrivateAddress: false", async () => {
|
|
1531
1769
|
await rejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
|
|
1532
1770
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
|
|
1533
1771
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
|
|
1534
1772
|
});
|
|
1535
1773
|
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
|
|
1536
|
-
await t.
|
|
1774
|
+
await t.step("allowPrivateAddress: true", async () => {
|
|
1537
1775
|
const expected = {
|
|
1538
1776
|
contextUrl: null,
|
|
1539
1777
|
documentUrl: "https://localhost/object",
|
|
@@ -1556,12 +1794,12 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1556
1794
|
headers: { Location: `https://example.com/too-many-redirects/${Number(cl.url.split("/").at(-1)) + 1}` }
|
|
1557
1795
|
};
|
|
1558
1796
|
});
|
|
1559
|
-
await t.
|
|
1797
|
+
await t.step("too many redirects", async () => {
|
|
1560
1798
|
redirectAttempts = 0;
|
|
1561
1799
|
await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
1562
1800
|
deepStrictEqual(redirectAttempts, 21);
|
|
1563
1801
|
});
|
|
1564
|
-
await t.
|
|
1802
|
+
await t.step("custom max redirection", async () => {
|
|
1565
1803
|
redirectAttempts = 0;
|
|
1566
1804
|
const loader = getDocumentLoader({ maxRedirection: 1 });
|
|
1567
1805
|
await rejects(() => loader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
@@ -1582,7 +1820,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1582
1820
|
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
1583
1821
|
};
|
|
1584
1822
|
});
|
|
1585
|
-
await t.
|
|
1823
|
+
await t.step("redirect loop", async () => {
|
|
1586
1824
|
loopAttempts = 0;
|
|
1587
1825
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
|
|
1588
1826
|
deepStrictEqual(loopAttempts, 2);
|
|
@@ -1595,7 +1833,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1595
1833
|
headers: { Location: "/redirect-loop-relative" }
|
|
1596
1834
|
};
|
|
1597
1835
|
});
|
|
1598
|
-
await t.
|
|
1836
|
+
await t.step("redirect loop with relative location", async () => {
|
|
1599
1837
|
relativeLoopAttempts = 0;
|
|
1600
1838
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
|
|
1601
1839
|
deepStrictEqual(relativeLoopAttempts, 1);
|
|
@@ -1605,7 +1843,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1605
1843
|
body: maliciousPayload,
|
|
1606
1844
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1607
1845
|
});
|
|
1608
|
-
await t.
|
|
1846
|
+
await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
1609
1847
|
const start = performance.now();
|
|
1610
1848
|
await rejects(() => fetchDocumentLoader("https://example.com/redos"), FetchError);
|
|
1611
1849
|
const elapsed = performance.now() - start;
|
|
@@ -1613,5 +1851,543 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1613
1851
|
});
|
|
1614
1852
|
esm_default.hardReset();
|
|
1615
1853
|
});
|
|
1854
|
+
test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
|
|
1855
|
+
const url = "https://example.com/bounded";
|
|
1856
|
+
esm_default.mockGlobal();
|
|
1857
|
+
let oversized = true;
|
|
1858
|
+
try {
|
|
1859
|
+
esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
|
|
1860
|
+
"Content-Type": "application/activity+json",
|
|
1861
|
+
...oversized ? { "Content-Length": String(16777217) } : {}
|
|
1862
|
+
} }));
|
|
1863
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1864
|
+
await rejects(loader(url), FetchError);
|
|
1865
|
+
oversized = false;
|
|
1866
|
+
deepStrictEqual((await loader(url)).document, { name: "hello" });
|
|
1867
|
+
oversized = true;
|
|
1868
|
+
esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
|
|
1869
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
|
|
1870
|
+
esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
|
|
1871
|
+
"Content-Type": "text/html",
|
|
1872
|
+
Link: `<${url}>; rel="alternate"; type="application/activity+json"`
|
|
1873
|
+
} }));
|
|
1874
|
+
await rejects(loader(`${url}/alternate`), FetchError);
|
|
1875
|
+
esm_default.get(`${url}/redirect`, {
|
|
1876
|
+
status: 302,
|
|
1877
|
+
headers: { Location: url }
|
|
1878
|
+
});
|
|
1879
|
+
await rejects(loader(`${url}/redirect`), FetchError);
|
|
1880
|
+
} finally {
|
|
1881
|
+
esm_default.hardReset();
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
|
|
1885
|
+
const document = { name: "a".repeat(16 * 1024 * 1024) };
|
|
1886
|
+
const compressed = gzipSync(JSON.stringify(document));
|
|
1887
|
+
ok(compressed.byteLength < 64 * 1024);
|
|
1888
|
+
const server = createServer((request, response) => {
|
|
1889
|
+
const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
|
|
1890
|
+
response.writeHead(200, {
|
|
1891
|
+
"Content-Type": "application/activity+json",
|
|
1892
|
+
"Content-Encoding": "gzip",
|
|
1893
|
+
"Content-Length": body.byteLength
|
|
1894
|
+
});
|
|
1895
|
+
response.end(body);
|
|
1896
|
+
});
|
|
1897
|
+
await new Promise((resolve, reject) => {
|
|
1898
|
+
server.once("error", reject);
|
|
1899
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1900
|
+
});
|
|
1901
|
+
try {
|
|
1902
|
+
const address = server.address();
|
|
1903
|
+
ok(address != null && typeof address !== "string");
|
|
1904
|
+
const url = `http://127.0.0.1:${address.port}/document`;
|
|
1905
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
|
|
1906
|
+
deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
|
|
1907
|
+
} finally {
|
|
1908
|
+
await new Promise((resolve, reject) => {
|
|
1909
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
1910
|
+
server.closeAllConnections();
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
test("getRemoteDocument() bounds JSON by default", async () => {
|
|
1915
|
+
let canceled = false;
|
|
1916
|
+
let pulls = 0;
|
|
1917
|
+
await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
|
|
1918
|
+
pull(controller) {
|
|
1919
|
+
pulls++;
|
|
1920
|
+
controller.enqueue(new Uint8Array(64 * 1024).fill(32));
|
|
1921
|
+
},
|
|
1922
|
+
cancel() {
|
|
1923
|
+
canceled = true;
|
|
1924
|
+
}
|
|
1925
|
+
}, { highWaterMark: 0 })), () => {
|
|
1926
|
+
throw new Error("Unexpected alternate document");
|
|
1927
|
+
}), FetchError);
|
|
1928
|
+
deepStrictEqual(canceled, true);
|
|
1929
|
+
deepStrictEqual(pulls, 257);
|
|
1930
|
+
});
|
|
1931
|
+
test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
1932
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
|
|
1933
|
+
const loader = getDocumentLoader();
|
|
1934
|
+
for (const result of [
|
|
1935
|
+
"throws",
|
|
1936
|
+
"empty",
|
|
1937
|
+
"private"
|
|
1938
|
+
]) await t.step(result, async () => {
|
|
1939
|
+
const originalLookup = dns.lookup;
|
|
1940
|
+
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1941
|
+
address: "127.0.0.1",
|
|
1942
|
+
family: 4
|
|
1943
|
+
}]));
|
|
1944
|
+
const records = [];
|
|
1945
|
+
await configure({
|
|
1946
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1947
|
+
loggers: [{
|
|
1948
|
+
category: "fedify",
|
|
1949
|
+
sinks: ["buffer"],
|
|
1950
|
+
lowestLevel: "debug"
|
|
1951
|
+
}, {
|
|
1952
|
+
category: ["logtape", "meta"],
|
|
1953
|
+
sinks: []
|
|
1954
|
+
}],
|
|
1955
|
+
reset: true
|
|
1956
|
+
});
|
|
1957
|
+
try {
|
|
1958
|
+
const url = "https://dns-failure.invalid/object";
|
|
1959
|
+
let error;
|
|
1960
|
+
await rejects(() => loader(url), (e) => {
|
|
1961
|
+
error = e;
|
|
1962
|
+
return true;
|
|
1963
|
+
});
|
|
1964
|
+
ok(error instanceof UrlError);
|
|
1965
|
+
deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
|
|
1966
|
+
deepStrictEqual(records.map((r) => [
|
|
1967
|
+
r.level,
|
|
1968
|
+
r.rawMessage,
|
|
1969
|
+
r.properties.url
|
|
1970
|
+
]), [result === "private" ? [
|
|
1971
|
+
"error",
|
|
1972
|
+
"Disallowed private URL: {url}",
|
|
1973
|
+
url
|
|
1974
|
+
] : [
|
|
1975
|
+
"debug",
|
|
1976
|
+
"DNS lookup failed for {url}",
|
|
1977
|
+
url
|
|
1978
|
+
]]);
|
|
1979
|
+
ok(records[0].properties.error === error);
|
|
1980
|
+
} finally {
|
|
1981
|
+
await reset();
|
|
1982
|
+
dns.lookup = originalLookup;
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
1987
|
+
const base = "https://example.com/alternate-chain/";
|
|
1988
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1989
|
+
for (const mode of [
|
|
1990
|
+
"header",
|
|
1991
|
+
"html",
|
|
1992
|
+
"mixed"
|
|
1993
|
+
]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
|
|
1994
|
+
esm_default.mockGlobal();
|
|
1995
|
+
let requests = 0;
|
|
1996
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1997
|
+
requests++;
|
|
1998
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1999
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
2000
|
+
const next = `${base}${index + 1}`;
|
|
2001
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
2002
|
+
return alternate(next, mode === "html");
|
|
2003
|
+
});
|
|
2004
|
+
try {
|
|
2005
|
+
if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
|
|
2006
|
+
else await rejects(loader(`${base}0`), (error) => {
|
|
2007
|
+
ok(error instanceof FetchError);
|
|
2008
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
2009
|
+
return true;
|
|
2010
|
+
});
|
|
2011
|
+
deepStrictEqual(requests, 21);
|
|
2012
|
+
} finally {
|
|
2013
|
+
esm_default.hardReset();
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
for (const mode of [
|
|
2017
|
+
"header",
|
|
2018
|
+
"html",
|
|
2019
|
+
"alternate-redirect",
|
|
2020
|
+
"redirect-alternate",
|
|
2021
|
+
"redirect-intermediate"
|
|
2022
|
+
]) await t.step(`${mode}: cycle`, async () => {
|
|
2023
|
+
esm_default.mockGlobal();
|
|
2024
|
+
let requests = 0;
|
|
2025
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
2026
|
+
requests++;
|
|
2027
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
2028
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
2029
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
2030
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
2031
|
+
return alternate(next, mode === "html");
|
|
2032
|
+
});
|
|
2033
|
+
try {
|
|
2034
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
2035
|
+
ok(error instanceof FetchError);
|
|
2036
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
2037
|
+
return true;
|
|
2038
|
+
});
|
|
2039
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
2040
|
+
} finally {
|
|
2041
|
+
esm_default.hardReset();
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
await t.step("alternate followed by 20 redirects shares the limit", async () => {
|
|
2045
|
+
esm_default.mockGlobal();
|
|
2046
|
+
let requests = 0;
|
|
2047
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
2048
|
+
requests++;
|
|
2049
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
2050
|
+
if (index === 21) return Response.json({ done: true });
|
|
2051
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
2052
|
+
});
|
|
2053
|
+
try {
|
|
2054
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
2055
|
+
ok(error instanceof FetchError);
|
|
2056
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
2057
|
+
return true;
|
|
2058
|
+
});
|
|
2059
|
+
deepStrictEqual(requests, 21);
|
|
2060
|
+
} finally {
|
|
2061
|
+
esm_default.hardReset();
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
await t.step("relative alternate after redirect and isolated calls", async () => {
|
|
2065
|
+
esm_default.mockGlobal();
|
|
2066
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
2067
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
2068
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
2069
|
+
try {
|
|
2070
|
+
for (let i = 0; i < 2; i++) {
|
|
2071
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
2072
|
+
for (const result of results) {
|
|
2073
|
+
deepStrictEqual(result.document, { done: true });
|
|
2074
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} finally {
|
|
2078
|
+
esm_default.hardReset();
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
function alternate(next, html) {
|
|
2082
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
2083
|
+
"Content-Type": "text/plain",
|
|
2084
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
2085
|
+
} });
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
2089
|
+
const base = "https://example.com/alternate-abort/";
|
|
2090
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
2091
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
2092
|
+
esm_default.mockGlobal();
|
|
2093
|
+
const controller = new AbortController();
|
|
2094
|
+
let requests = 0;
|
|
2095
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
2096
|
+
requests++;
|
|
2097
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
2098
|
+
if (index === 2) return Response.json({ done: true });
|
|
2099
|
+
if (index === 1) controller.abort();
|
|
2100
|
+
const next = `${base}${index + 1}`;
|
|
2101
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
2102
|
+
"Content-Type": "text/plain",
|
|
2103
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
2104
|
+
} });
|
|
2105
|
+
});
|
|
2106
|
+
try {
|
|
2107
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
|
|
2108
|
+
deepStrictEqual(requests, 2);
|
|
2109
|
+
} finally {
|
|
2110
|
+
esm_default.hardReset();
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
test("getDocumentLoader() rejects cancellation before fetching", async () => {
|
|
2115
|
+
esm_default.mockGlobal();
|
|
2116
|
+
let requests = 0;
|
|
2117
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
2118
|
+
esm_default.get(url, () => {
|
|
2119
|
+
requests++;
|
|
2120
|
+
return Response.json({ done: true });
|
|
2121
|
+
});
|
|
2122
|
+
try {
|
|
2123
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
2124
|
+
const controller = new AbortController();
|
|
2125
|
+
controller.abort();
|
|
2126
|
+
await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
2127
|
+
deepStrictEqual(requests, 0);
|
|
2128
|
+
} finally {
|
|
2129
|
+
esm_default.hardReset();
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
async function withServer(handler, callback) {
|
|
2133
|
+
const server = createServer(handler);
|
|
2134
|
+
await new Promise((resolve, reject) => {
|
|
2135
|
+
server.once("error", reject);
|
|
2136
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
2137
|
+
});
|
|
2138
|
+
try {
|
|
2139
|
+
const address = server.address();
|
|
2140
|
+
ok(address != null && typeof address !== "string");
|
|
2141
|
+
return await callback(`http://127.0.0.1:${address.port}`);
|
|
2142
|
+
} finally {
|
|
2143
|
+
await new Promise((resolve, reject) => {
|
|
2144
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
2145
|
+
server.closeAllConnections();
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function isTimeoutFetchError(error, url) {
|
|
2150
|
+
ok(error instanceof FetchError, String(error));
|
|
2151
|
+
deepStrictEqual(error.url.href, url);
|
|
2152
|
+
deepStrictEqual(error.response, void 0);
|
|
2153
|
+
ok(error.cause instanceof DOMException);
|
|
2154
|
+
deepStrictEqual(error.cause.name, "TimeoutError");
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
test("resolveDocumentLoaderTimeout() validates timeouts", () => {
|
|
2158
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(void 0), 1e4);
|
|
2159
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(null), null);
|
|
2160
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(1500), 1500);
|
|
2161
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(.5), 1);
|
|
2162
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(2147483647), 2147483647);
|
|
2163
|
+
for (const invalid of [
|
|
2164
|
+
0,
|
|
2165
|
+
-1,
|
|
2166
|
+
NaN,
|
|
2167
|
+
Infinity,
|
|
2168
|
+
2147483648
|
|
2169
|
+
]) throws(() => resolveDocumentLoaderTimeout(invalid), RangeError);
|
|
2170
|
+
throws(() => getDocumentLoader({ timeout: 0 }), RangeError);
|
|
2171
|
+
});
|
|
2172
|
+
test("getDocumentLoader() times out a request without a response", async () => {
|
|
2173
|
+
await withServer(() => {}, async (baseUrl) => {
|
|
2174
|
+
const url = `${baseUrl}/hang`;
|
|
2175
|
+
await rejects(getDocumentLoader({
|
|
2176
|
+
allowPrivateAddress: true,
|
|
2177
|
+
timeout: 100
|
|
2178
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
test("getDocumentLoader() times out a stalled response body", async () => {
|
|
2182
|
+
for (const status of [200, 404]) await withServer((_, response) => {
|
|
2183
|
+
response.writeHead(status, { "Content-Type": "application/activity+json" });
|
|
2184
|
+
response.write("{\"id\":");
|
|
2185
|
+
}, async (baseUrl) => {
|
|
2186
|
+
const url = `${baseUrl}/stalled`;
|
|
2187
|
+
await rejects(getDocumentLoader({
|
|
2188
|
+
allowPrivateAddress: true,
|
|
2189
|
+
timeout: 200
|
|
2190
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2191
|
+
});
|
|
2192
|
+
});
|
|
2193
|
+
test("getDocumentLoader() shares the timeout across redirects", async () => {
|
|
2194
|
+
let requests = 0;
|
|
2195
|
+
await withServer((request, response) => {
|
|
2196
|
+
requests++;
|
|
2197
|
+
const index = Number(request.url?.split("/").at(-1));
|
|
2198
|
+
setTimeout(() => {
|
|
2199
|
+
if (index >= 15) {
|
|
2200
|
+
response.writeHead(200, { "Content-Type": "application/activity+json" });
|
|
2201
|
+
response.end("{\"done\":true}");
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
response.writeHead(302, { Location: `/redirect/${index + 1}` });
|
|
2205
|
+
response.end();
|
|
2206
|
+
}, 50);
|
|
2207
|
+
}, async (baseUrl) => {
|
|
2208
|
+
const url = `${baseUrl}/redirect/0`;
|
|
2209
|
+
await rejects(getDocumentLoader({
|
|
2210
|
+
allowPrivateAddress: true,
|
|
2211
|
+
timeout: 300
|
|
2212
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2213
|
+
ok(requests < 16, `requests: ${requests}`);
|
|
2214
|
+
deepStrictEqual((await getDocumentLoader({
|
|
2215
|
+
allowPrivateAddress: true,
|
|
2216
|
+
timeout: null
|
|
2217
|
+
})(url)).document, { done: true });
|
|
2218
|
+
});
|
|
2219
|
+
});
|
|
2220
|
+
test("getDocumentLoader() shares the timeout across alternates", async (t) => {
|
|
2221
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
2222
|
+
await withServer((request, response) => {
|
|
2223
|
+
const index = Number(request.url?.split("/").at(-1));
|
|
2224
|
+
setTimeout(() => {
|
|
2225
|
+
if (index >= 15) {
|
|
2226
|
+
response.writeHead(200, { "Content-Type": "application/activity+json" });
|
|
2227
|
+
response.end("{\"done\":true}");
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const next = `/alternate/${index + 1}`;
|
|
2231
|
+
response.writeHead(200, html ? { "Content-Type": "text/html" } : {
|
|
2232
|
+
"Content-Type": "text/plain",
|
|
2233
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
2234
|
+
});
|
|
2235
|
+
response.end(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON");
|
|
2236
|
+
}, 50);
|
|
2237
|
+
}, async (baseUrl) => {
|
|
2238
|
+
const url = `${baseUrl}/alternate/0`;
|
|
2239
|
+
await rejects(getDocumentLoader({
|
|
2240
|
+
allowPrivateAddress: true,
|
|
2241
|
+
timeout: 300
|
|
2242
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2243
|
+
});
|
|
2244
|
+
});
|
|
2245
|
+
});
|
|
2246
|
+
test("getDocumentLoader() lets the caller's signal abort a request", async () => {
|
|
2247
|
+
await withServer(() => {}, async (baseUrl) => {
|
|
2248
|
+
const url = `${baseUrl}/hang`;
|
|
2249
|
+
for (const timeout of [
|
|
2250
|
+
void 0,
|
|
2251
|
+
6e4,
|
|
2252
|
+
null
|
|
2253
|
+
]) {
|
|
2254
|
+
const loader = getDocumentLoader({
|
|
2255
|
+
allowPrivateAddress: true,
|
|
2256
|
+
timeout
|
|
2257
|
+
});
|
|
2258
|
+
const controller = new AbortController();
|
|
2259
|
+
const reason = /* @__PURE__ */ new Error("Canceled by the caller");
|
|
2260
|
+
setTimeout(() => controller.abort(reason), 50);
|
|
2261
|
+
await rejects(loader(url, { signal: controller.signal }), (e) => {
|
|
2262
|
+
deepStrictEqual(e, reason);
|
|
2263
|
+
return true;
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
const loader = getDocumentLoader({
|
|
2267
|
+
allowPrivateAddress: true,
|
|
2268
|
+
timeout: 50
|
|
2269
|
+
});
|
|
2270
|
+
const controller = new AbortController();
|
|
2271
|
+
const reason = /* @__PURE__ */ new Error("Canceled by the caller");
|
|
2272
|
+
controller.abort(reason);
|
|
2273
|
+
await rejects(loader(url, { signal: controller.signal }), (e) => {
|
|
2274
|
+
deepStrictEqual(e, reason);
|
|
2275
|
+
return true;
|
|
2276
|
+
});
|
|
2277
|
+
});
|
|
2278
|
+
});
|
|
2279
|
+
test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async () => {
|
|
2280
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
|
|
2281
|
+
esm_default.mockGlobal();
|
|
2282
|
+
let requests = 0;
|
|
2283
|
+
const url = "https://slow-dns.example/object";
|
|
2284
|
+
esm_default.get(url, () => {
|
|
2285
|
+
requests++;
|
|
2286
|
+
return Response.json({ id: url });
|
|
2287
|
+
});
|
|
2288
|
+
const originalLookup = dns.lookup;
|
|
2289
|
+
let resolveLookup = () => {};
|
|
2290
|
+
const lookedUp = new Promise((resolve) => resolveLookup = resolve);
|
|
2291
|
+
dns.lookup = (() => new Promise((resolve) => setTimeout(() => {
|
|
2292
|
+
resolve([{
|
|
2293
|
+
address: "93.184.215.14",
|
|
2294
|
+
family: 4
|
|
2295
|
+
}]);
|
|
2296
|
+
resolveLookup();
|
|
2297
|
+
}, 200)));
|
|
2298
|
+
try {
|
|
2299
|
+
await rejects(getDocumentLoader({ timeout: 50 })(url), (e) => isTimeoutFetchError(e, url));
|
|
2300
|
+
await lookedUp;
|
|
2301
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2302
|
+
deepStrictEqual(requests, 0);
|
|
2303
|
+
} finally {
|
|
2304
|
+
dns.lookup = originalLookup;
|
|
2305
|
+
esm_default.hardReset();
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
test("withDocumentLoaderTimeout() cleans up after each call", async () => {
|
|
2309
|
+
const signals = [];
|
|
2310
|
+
const loader = withDocumentLoaderTimeout((url, options) => {
|
|
2311
|
+
signals.push(options.signal);
|
|
2312
|
+
return Promise.resolve({
|
|
2313
|
+
contextUrl: null,
|
|
2314
|
+
document: {},
|
|
2315
|
+
documentUrl: url
|
|
2316
|
+
});
|
|
2317
|
+
}, 50);
|
|
2318
|
+
deepStrictEqual((await Promise.all([loader("https://example.com/a"), loader("https://example.com/b")])).map((r) => r.documentUrl), ["https://example.com/a", "https://example.com/b"]);
|
|
2319
|
+
ok(signals[0] !== signals[1]);
|
|
2320
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2321
|
+
ok(signals.every((signal) => !signal.aborted));
|
|
2322
|
+
const inner = () => Promise.reject(/* @__PURE__ */ new Error("unused"));
|
|
2323
|
+
ok(withDocumentLoaderTimeout(inner, null) === inner);
|
|
2324
|
+
});
|
|
2325
|
+
test("getRemoteDocument() keeps error bodies byte for byte", async () => {
|
|
2326
|
+
const bytes = new Uint8Array([
|
|
2327
|
+
239,
|
|
2328
|
+
187,
|
|
2329
|
+
191,
|
|
2330
|
+
78,
|
|
2331
|
+
111,
|
|
2332
|
+
255,
|
|
2333
|
+
254
|
|
2334
|
+
]);
|
|
2335
|
+
const url = "https://example.com/error";
|
|
2336
|
+
let error;
|
|
2337
|
+
await rejects(getRemoteDocument(url, new Response(bytes, { status: 404 }), () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
|
|
2338
|
+
error = e;
|
|
2339
|
+
return true;
|
|
2340
|
+
});
|
|
2341
|
+
ok(error instanceof FetchError);
|
|
2342
|
+
ok(error.response != null);
|
|
2343
|
+
deepStrictEqual(error.response.status, 404);
|
|
2344
|
+
deepStrictEqual(new Uint8Array(await error.response.arrayBuffer()), bytes);
|
|
2345
|
+
});
|
|
2346
|
+
test("getRemoteDocument() keeps metadata of bodiless or large errors", async () => {
|
|
2347
|
+
const url = "https://example.com/error";
|
|
2348
|
+
for (const response of [new Response(null, {
|
|
2349
|
+
status: 304,
|
|
2350
|
+
headers: { ETag: "\"a\"" }
|
|
2351
|
+
}), new Response(/* @__PURE__ */ new Uint8Array(1048577), {
|
|
2352
|
+
status: 500,
|
|
2353
|
+
headers: { ETag: "\"a\"" }
|
|
2354
|
+
})]) await rejects(getRemoteDocument(url, response, () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
|
|
2355
|
+
ok(e instanceof FetchError);
|
|
2356
|
+
ok(e.response != null);
|
|
2357
|
+
deepStrictEqual(e.response.status, response.status);
|
|
2358
|
+
deepStrictEqual(e.response.headers.get("ETag"), "\"a\"");
|
|
2359
|
+
deepStrictEqual(e.response.body, null);
|
|
2360
|
+
return true;
|
|
2361
|
+
});
|
|
2362
|
+
});
|
|
2363
|
+
test("getDocumentLoader() reports statuses that Response cannot hold", async () => {
|
|
2364
|
+
await withServer((_, response) => {
|
|
2365
|
+
response.writeHead(999);
|
|
2366
|
+
response.end("Request denied");
|
|
2367
|
+
}, async (baseUrl) => {
|
|
2368
|
+
const url = `${baseUrl}/denied`;
|
|
2369
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
2370
|
+
let error;
|
|
2371
|
+
await rejects(loader(url), (e) => {
|
|
2372
|
+
error = e;
|
|
2373
|
+
return true;
|
|
2374
|
+
});
|
|
2375
|
+
ok(error instanceof FetchError, String(error));
|
|
2376
|
+
deepStrictEqual(error.response?.status, 999);
|
|
2377
|
+
deepStrictEqual(await error.response.text(), "Request denied");
|
|
2378
|
+
});
|
|
2379
|
+
});
|
|
2380
|
+
test("getDocumentLoader() times out a stalled body of such a status", async () => {
|
|
2381
|
+
await withServer((_, response) => {
|
|
2382
|
+
response.writeHead(999);
|
|
2383
|
+
response.write("Request");
|
|
2384
|
+
}, async (baseUrl) => {
|
|
2385
|
+
const url = `${baseUrl}/stalled-denied`;
|
|
2386
|
+
await rejects(getDocumentLoader({
|
|
2387
|
+
allowPrivateAddress: true,
|
|
2388
|
+
timeout: 200
|
|
2389
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
1616
2392
|
//#endregion
|
|
1617
2393
|
export {};
|