@j0hanz/superfetch 1.2.5 → 2.0.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/README.md +131 -156
- package/dist/config/auth-config.d.ts +16 -0
- package/dist/config/auth-config.js +53 -0
- package/dist/config/constants.d.ts +11 -13
- package/dist/config/constants.js +1 -3
- package/dist/config/env-parsers.d.ts +7 -0
- package/dist/config/env-parsers.js +84 -0
- package/dist/config/formatting.d.ts +2 -2
- package/dist/config/index.d.ts +47 -53
- package/dist/config/index.js +35 -64
- package/dist/config/types/content.d.ts +1 -49
- package/dist/config/types/runtime.d.ts +8 -16
- package/dist/config/types/tools.d.ts +2 -28
- package/dist/http/accept-policy.d.ts +3 -0
- package/dist/http/accept-policy.js +45 -0
- package/dist/http/async-handler.d.ts +2 -0
- package/dist/http/async-handler.js +5 -0
- package/dist/http/auth-introspection.d.ts +2 -0
- package/dist/http/auth-introspection.js +141 -0
- package/dist/http/auth-static.d.ts +2 -0
- package/dist/http/auth-static.js +23 -0
- package/dist/http/auth.d.ts +3 -2
- package/dist/http/auth.js +254 -23
- package/dist/http/cors.d.ts +6 -6
- package/dist/http/cors.js +7 -42
- package/dist/http/download-routes.d.ts +0 -12
- package/dist/http/download-routes.js +21 -58
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/jsonrpc-http.d.ts +2 -0
- package/dist/http/jsonrpc-http.js +10 -0
- package/dist/http/mcp-routes.d.ts +8 -3
- package/dist/http/mcp-routes.js +137 -31
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-helpers.d.ts +0 -1
- package/dist/http/mcp-session-helpers.js +1 -1
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-transport.d.ts +7 -0
- package/dist/http/mcp-session-transport.js +57 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +15 -137
- package/dist/http/mcp-sessions.d.ts +43 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/mcp-validation.d.ts +1 -0
- package/dist/http/mcp-validation.js +11 -10
- package/dist/http/protocol-policy.d.ts +2 -0
- package/dist/http/protocol-policy.js +31 -0
- package/dist/http/rate-limit.js +7 -4
- package/dist/http/server-config.d.ts +1 -0
- package/dist/http/server-config.js +40 -0
- package/dist/http/server-middleware.d.ts +7 -9
- package/dist/http/server-middleware.js +9 -70
- package/dist/http/server-shutdown.d.ts +4 -0
- package/dist/http/server-shutdown.js +43 -0
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +546 -61
- package/dist/http/session-cleanup.js +8 -5
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +32 -33
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +67 -125
- package/dist/resources/index.js +0 -82
- package/dist/server.js +50 -29
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache-keys.d.ts +7 -0
- package/dist/services/cache-keys.js +57 -0
- package/dist/services/cache.d.ts +4 -9
- package/dist/services/cache.js +77 -139
- package/dist/services/context.d.ts +0 -1
- package/dist/services/context.js +0 -7
- package/dist/services/extractor.js +55 -116
- package/dist/services/fetcher/agents.d.ts +2 -2
- package/dist/services/fetcher/agents.js +35 -96
- package/dist/services/fetcher/dns-selection.d.ts +2 -0
- package/dist/services/fetcher/dns-selection.js +72 -0
- package/dist/services/fetcher/interceptors.d.ts +0 -22
- package/dist/services/fetcher/interceptors.js +18 -32
- package/dist/services/fetcher/redirects.js +16 -7
- package/dist/services/fetcher/response.js +79 -34
- package/dist/services/fetcher.d.ts +22 -3
- package/dist/services/fetcher.js +544 -44
- package/dist/services/fifo-queue.d.ts +8 -0
- package/dist/services/fifo-queue.js +25 -0
- package/dist/services/logger.js +2 -2
- package/dist/services/metadata-collector.d.ts +1 -9
- package/dist/services/metadata-collector.js +71 -2
- package/dist/services/transform-worker-pool.d.ts +4 -14
- package/dist/services/transform-worker-pool.js +177 -129
- package/dist/services/transform-worker-types.d.ts +32 -0
- package/dist/services/transform-worker-types.js +14 -0
- package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
- package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
- package/dist/tools/handlers/fetch-single.shared.js +175 -89
- package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
- package/dist/tools/handlers/fetch-url.tool.js +84 -119
- package/dist/tools/index.js +21 -40
- package/dist/tools/schemas.d.ts +1 -51
- package/dist/tools/schemas.js +1 -107
- package/dist/tools/utils/cached-markdown.d.ts +5 -0
- package/dist/tools/utils/cached-markdown.js +46 -0
- package/dist/tools/utils/content-shaping.d.ts +4 -0
- package/dist/tools/utils/content-shaping.js +67 -0
- package/dist/tools/utils/content-transform.d.ts +5 -17
- package/dist/tools/utils/content-transform.js +134 -114
- package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
- package/dist/tools/utils/fetch-pipeline.js +57 -63
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/inline-content.d.ts +1 -2
- package/dist/tools/utils/inline-content.js +4 -7
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +135 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +2 -0
- package/dist/transformers/markdown.js +185 -0
- package/dist/transformers/markdown.transformer.js +5 -117
- package/dist/utils/cached-payload.d.ts +7 -0
- package/dist/utils/cached-payload.js +36 -0
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.d.ts +9 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/error-utils.js +1 -1
- package/dist/utils/filename-generator.js +34 -12
- package/dist/utils/guards.d.ts +1 -0
- package/dist/utils/guards.js +3 -0
- package/dist/utils/header-normalizer.d.ts +0 -3
- package/dist/utils/header-normalizer.js +3 -3
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.d.ts +2 -2
- package/dist/utils/tool-error-handler.js +14 -46
- package/dist/utils/url-transformer.d.ts +7 -0
- package/dist/utils/url-transformer.js +147 -0
- package/dist/utils/url-validator.d.ts +1 -2
- package/dist/utils/url-validator.js +53 -114
- package/dist/workers/content-transform.worker.d.ts +1 -0
- package/dist/workers/content-transform.worker.js +40 -0
- package/package.json +17 -18
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
import dns from 'node:dns';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { Agent } from 'undici';
|
|
4
|
-
import { createErrorWithCode } from '../../utils/error-
|
|
5
|
-
import {
|
|
4
|
+
import { createErrorWithCode } from '../../utils/error-details.js';
|
|
5
|
+
import { isRecord } from '../../utils/guards.js';
|
|
6
|
+
import { handleLookupResult } from './dns-selection.js';
|
|
6
7
|
const DNS_LOOKUP_TIMEOUT_MS = 5000;
|
|
7
8
|
function resolveDns(hostname, options, callback) {
|
|
8
9
|
const { normalizedOptions, useAll, resolvedFamily } = buildLookupContext(options);
|
|
9
10
|
const lookupOptions = buildLookupOptions(normalizedOptions);
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
if (done)
|
|
13
|
-
return;
|
|
14
|
-
done = true;
|
|
15
|
-
callback(createErrorWithCode(`DNS lookup timed out for ${hostname}`, 'ETIMEOUT'), []);
|
|
16
|
-
}, DNS_LOOKUP_TIMEOUT_MS);
|
|
17
|
-
timer.unref();
|
|
18
|
-
const safeCallback = (err, address, family) => {
|
|
19
|
-
if (done)
|
|
20
|
-
return;
|
|
21
|
-
done = true;
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
callback(err, address, family);
|
|
24
|
-
};
|
|
11
|
+
const timeout = createLookupTimeout(hostname, callback);
|
|
12
|
+
const safeCallback = wrapLookupCallback(callback, timeout);
|
|
25
13
|
dns.lookup(hostname, lookupOptions, createLookupCallback(hostname, resolvedFamily, useAll, safeCallback));
|
|
26
14
|
}
|
|
27
15
|
function normalizeLookupOptions(options) {
|
|
@@ -46,17 +34,19 @@ function resolveResultOrder(options) {
|
|
|
46
34
|
return DEFAULT_DNS_ORDER;
|
|
47
35
|
}
|
|
48
36
|
function getLegacyVerbatim(options) {
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
if (isRecord(options)) {
|
|
38
|
+
const { verbatim } = options;
|
|
39
|
+
return typeof verbatim === 'boolean' ? verbatim : undefined;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
51
42
|
}
|
|
52
43
|
function buildLookupOptions(normalizedOptions) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
return {
|
|
45
|
+
family: normalizedOptions.family,
|
|
46
|
+
hints: normalizedOptions.hints,
|
|
56
47
|
all: true,
|
|
48
|
+
order: resolveResultOrder(normalizedOptions),
|
|
57
49
|
};
|
|
58
|
-
delete options.verbatim;
|
|
59
|
-
return options;
|
|
60
50
|
}
|
|
61
51
|
function createLookupCallback(hostname, resolvedFamily, useAll, callback) {
|
|
62
52
|
return (err, addresses) => {
|
|
@@ -70,81 +60,30 @@ function resolveFamily(family) {
|
|
|
70
60
|
return 6;
|
|
71
61
|
return family;
|
|
72
62
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const list = normalizeLookupResults(addresses, resolvedFamily);
|
|
85
|
-
const invalidFamilyError = findInvalidFamilyError(list, hostname);
|
|
86
|
-
if (invalidFamilyError) {
|
|
87
|
-
callback(invalidFamilyError, list);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const blockedError = findBlockedIpError(list, hostname);
|
|
91
|
-
if (blockedError) {
|
|
92
|
-
callback(blockedError, list);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const selection = selectLookupResult(list, useAll, hostname);
|
|
96
|
-
if (selection.error) {
|
|
97
|
-
callback(selection.error, selection.fallback);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
callback(null, selection.address, selection.family);
|
|
101
|
-
}
|
|
102
|
-
function selectLookupResult(list, useAll, hostname) {
|
|
103
|
-
if (list.length === 0) {
|
|
104
|
-
return {
|
|
105
|
-
error: createNoDnsResultsError(hostname),
|
|
106
|
-
fallback: [],
|
|
107
|
-
address: [],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (useAll) {
|
|
111
|
-
return { address: list, fallback: list };
|
|
112
|
-
}
|
|
113
|
-
const first = list.at(0);
|
|
114
|
-
if (!first) {
|
|
115
|
-
return {
|
|
116
|
-
error: createNoDnsResultsError(hostname),
|
|
117
|
-
fallback: [],
|
|
118
|
-
address: [],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
63
|
+
function createLookupTimeout(hostname, callback) {
|
|
64
|
+
let done = false;
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
if (done)
|
|
67
|
+
return;
|
|
68
|
+
done = true;
|
|
69
|
+
callback(createErrorWithCode(`DNS lookup timed out for ${hostname}`, 'ETIMEOUT'), []);
|
|
70
|
+
}, DNS_LOOKUP_TIMEOUT_MS);
|
|
71
|
+
timer.unref();
|
|
121
72
|
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
73
|
+
isDone: () => done,
|
|
74
|
+
markDone: () => {
|
|
75
|
+
done = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
},
|
|
125
78
|
};
|
|
126
79
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
function findInvalidFamilyError(list, hostname) {
|
|
138
|
-
for (const addr of list) {
|
|
139
|
-
const family = typeof addr === 'string' ? 0 : addr.family;
|
|
140
|
-
if (family === 4 || family === 6)
|
|
141
|
-
continue;
|
|
142
|
-
return createErrorWithCode(`Invalid address family returned for ${hostname}`, 'EINVAL');
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
function createNoDnsResultsError(hostname) {
|
|
147
|
-
return createErrorWithCode(`No DNS results returned for ${hostname}`, 'ENODATA');
|
|
80
|
+
function wrapLookupCallback(callback, timeout) {
|
|
81
|
+
return (err, address, family) => {
|
|
82
|
+
if (timeout.isDone())
|
|
83
|
+
return;
|
|
84
|
+
timeout.markDone();
|
|
85
|
+
callback(err, address, family);
|
|
86
|
+
};
|
|
148
87
|
}
|
|
149
88
|
function getAgentOptions() {
|
|
150
89
|
const cpuCount = os.availableParallelism();
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { LookupAddress } from 'node:dns';
|
|
2
|
+
export declare function handleLookupResult(error: NodeJS.ErrnoException | null, addresses: string | LookupAddress[], hostname: string, resolvedFamily: number | undefined, useAll: boolean, callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createErrorWithCode } from '../../utils/error-details.js';
|
|
2
|
+
import { isBlockedIp } from '../../utils/url-validator.js';
|
|
3
|
+
function normalizeLookupResults(addresses, family) {
|
|
4
|
+
if (Array.isArray(addresses)) {
|
|
5
|
+
return addresses;
|
|
6
|
+
}
|
|
7
|
+
return [{ address: addresses, family: family ?? 4 }];
|
|
8
|
+
}
|
|
9
|
+
function findBlockedIpError(list, hostname) {
|
|
10
|
+
for (const addr of list) {
|
|
11
|
+
const ip = typeof addr === 'string' ? addr : addr.address;
|
|
12
|
+
if (!isBlockedIp(ip)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
return createErrorWithCode(`Blocked IP detected for ${hostname}`, 'EBLOCKED');
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function findInvalidFamilyError(list, hostname) {
|
|
20
|
+
for (const addr of list) {
|
|
21
|
+
const family = typeof addr === 'string' ? 0 : addr.family;
|
|
22
|
+
if (family === 4 || family === 6)
|
|
23
|
+
continue;
|
|
24
|
+
return createErrorWithCode(`Invalid address family returned for ${hostname}`, 'EINVAL');
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function createNoDnsResultsError(hostname) {
|
|
29
|
+
return createErrorWithCode(`No DNS results returned for ${hostname}`, 'ENODATA');
|
|
30
|
+
}
|
|
31
|
+
function createEmptySelection(hostname) {
|
|
32
|
+
return {
|
|
33
|
+
error: createNoDnsResultsError(hostname),
|
|
34
|
+
fallback: [],
|
|
35
|
+
address: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function selectLookupResult(list, useAll, hostname) {
|
|
39
|
+
if (list.length === 0)
|
|
40
|
+
return createEmptySelection(hostname);
|
|
41
|
+
if (useAll)
|
|
42
|
+
return { address: list, fallback: list };
|
|
43
|
+
const first = list.at(0);
|
|
44
|
+
if (!first)
|
|
45
|
+
return createEmptySelection(hostname);
|
|
46
|
+
return {
|
|
47
|
+
address: first.address,
|
|
48
|
+
family: first.family,
|
|
49
|
+
fallback: list,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function findLookupError(list, hostname) {
|
|
53
|
+
return (findInvalidFamilyError(list, hostname) ?? findBlockedIpError(list, hostname));
|
|
54
|
+
}
|
|
55
|
+
export function handleLookupResult(error, addresses, hostname, resolvedFamily, useAll, callback) {
|
|
56
|
+
if (error) {
|
|
57
|
+
callback(error, addresses);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const list = normalizeLookupResults(addresses, resolvedFamily);
|
|
61
|
+
const lookupError = findLookupError(list, hostname);
|
|
62
|
+
if (lookupError) {
|
|
63
|
+
callback(lookupError, list);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const selection = selectLookupResult(list, useAll, hostname);
|
|
67
|
+
if (selection.error) {
|
|
68
|
+
callback(selection.error, selection.fallback);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
callback(null, selection.address, selection.family);
|
|
72
|
+
}
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
export type FetchChannelEvent = {
|
|
2
|
-
v: 1;
|
|
3
|
-
type: 'start';
|
|
4
|
-
requestId: string;
|
|
5
|
-
method: string;
|
|
6
|
-
url: string;
|
|
7
|
-
} | {
|
|
8
|
-
v: 1;
|
|
9
|
-
type: 'end';
|
|
10
|
-
requestId: string;
|
|
11
|
-
status: number;
|
|
12
|
-
duration: number;
|
|
13
|
-
} | {
|
|
14
|
-
v: 1;
|
|
15
|
-
type: 'error';
|
|
16
|
-
requestId: string;
|
|
17
|
-
url: string;
|
|
18
|
-
error: string;
|
|
19
|
-
code?: string;
|
|
20
|
-
status?: number;
|
|
21
|
-
duration: number;
|
|
22
|
-
};
|
|
23
1
|
interface FetchTelemetryContext {
|
|
24
2
|
requestId: string;
|
|
25
3
|
startTime: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import diagnosticsChannel from 'node:diagnostics_channel';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
|
-
import { isSystemError } from '../../utils/error-
|
|
4
|
+
import { isSystemError } from '../../utils/error-details.js';
|
|
5
5
|
import { logDebug, logError, logWarn } from '../logger.js';
|
|
6
6
|
const fetchChannel = diagnosticsChannel.channel('superfetch.fetch');
|
|
7
7
|
function redactUrl(rawUrl) {
|
|
@@ -51,51 +51,36 @@ export function startFetchTelemetry(url, method) {
|
|
|
51
51
|
}
|
|
52
52
|
export function recordFetchResponse(context, response, contentSize) {
|
|
53
53
|
const duration = performance.now() - context.startTime;
|
|
54
|
-
|
|
55
|
-
logDebug('HTTP Response', {
|
|
56
|
-
requestId: context.requestId,
|
|
57
|
-
status: response.status,
|
|
58
|
-
url: context.url,
|
|
59
|
-
...buildResponseMeta(response, contentSize, duration),
|
|
60
|
-
});
|
|
61
|
-
logSlowRequestIfNeeded(context, duration);
|
|
62
|
-
}
|
|
63
|
-
function publishFetchEnd(context, status, duration) {
|
|
54
|
+
const durationLabel = `${Math.round(duration)}ms`;
|
|
64
55
|
publishFetchEvent({
|
|
65
56
|
v: 1,
|
|
66
57
|
type: 'end',
|
|
67
58
|
requestId: context.requestId,
|
|
68
|
-
status,
|
|
59
|
+
status: response.status,
|
|
69
60
|
duration,
|
|
70
61
|
});
|
|
71
|
-
}
|
|
72
|
-
function buildResponseMeta(response, contentSize, duration) {
|
|
73
|
-
const contentLength = response.headers.get('content-length') ?? contentSize?.toString();
|
|
74
|
-
const meta = {
|
|
75
|
-
duration: `${Math.round(duration)}ms`,
|
|
76
|
-
};
|
|
77
62
|
const contentType = response.headers.get('content-type');
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (contentLength !== undefined) {
|
|
82
|
-
meta.size = contentLength;
|
|
83
|
-
}
|
|
84
|
-
return meta;
|
|
85
|
-
}
|
|
86
|
-
function logSlowRequestIfNeeded(context, duration) {
|
|
87
|
-
if (duration <= 5000)
|
|
88
|
-
return;
|
|
89
|
-
logWarn('Slow HTTP request detected', {
|
|
63
|
+
const contentLength = response.headers.get('content-length') ??
|
|
64
|
+
(contentSize === undefined ? undefined : String(contentSize));
|
|
65
|
+
logDebug('HTTP Response', {
|
|
90
66
|
requestId: context.requestId,
|
|
67
|
+
status: response.status,
|
|
91
68
|
url: context.url,
|
|
92
|
-
duration:
|
|
69
|
+
duration: durationLabel,
|
|
70
|
+
...(contentType ? { contentType } : {}),
|
|
71
|
+
...(contentLength ? { size: contentLength } : {}),
|
|
93
72
|
});
|
|
73
|
+
if (duration > 5000) {
|
|
74
|
+
logWarn('Slow HTTP request detected', {
|
|
75
|
+
requestId: context.requestId,
|
|
76
|
+
url: context.url,
|
|
77
|
+
duration: durationLabel,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
94
80
|
}
|
|
95
81
|
export function recordFetchError(context, error, status) {
|
|
96
82
|
const duration = performance.now() - context.startTime;
|
|
97
83
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
98
|
-
const code = isSystemError(err) ? err.code : undefined;
|
|
99
84
|
const event = {
|
|
100
85
|
v: 1,
|
|
101
86
|
type: 'error',
|
|
@@ -104,6 +89,7 @@ export function recordFetchError(context, error, status) {
|
|
|
104
89
|
error: err.message,
|
|
105
90
|
duration,
|
|
106
91
|
};
|
|
92
|
+
const code = isSystemError(err) ? err.code : undefined;
|
|
107
93
|
if (code !== undefined) {
|
|
108
94
|
event.code = code;
|
|
109
95
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { FetchError } from '../../errors/app-error.js';
|
|
2
|
-
import { createErrorWithCode } from '../../utils/error-
|
|
2
|
+
import { createErrorWithCode } from '../../utils/error-details.js';
|
|
3
|
+
import { isRecord } from '../../utils/guards.js';
|
|
3
4
|
import { validateAndNormalizeUrl } from '../../utils/url-validator.js';
|
|
4
5
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
5
6
|
function isRedirectStatus(status) {
|
|
6
7
|
return REDIRECT_STATUSES.has(status);
|
|
7
8
|
}
|
|
9
|
+
function cancelResponseBody(response) {
|
|
10
|
+
const cancelPromise = response.body?.cancel();
|
|
11
|
+
if (cancelPromise) {
|
|
12
|
+
cancelPromise.catch(() => {
|
|
13
|
+
// Best-effort cancellation; ignore failures.
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
async function performFetchCycle(currentUrl, init, redirectLimit, redirectCount) {
|
|
9
18
|
const response = await fetch(currentUrl, { ...init, redirect: 'manual' });
|
|
10
19
|
if (!isRedirectStatus(response.status)) {
|
|
@@ -12,31 +21,31 @@ async function performFetchCycle(currentUrl, init, redirectLimit, redirectCount)
|
|
|
12
21
|
}
|
|
13
22
|
assertRedirectWithinLimit(response, currentUrl, redirectLimit, redirectCount);
|
|
14
23
|
const location = getRedirectLocation(response, currentUrl);
|
|
15
|
-
|
|
24
|
+
cancelResponseBody(response);
|
|
16
25
|
return {
|
|
17
26
|
response,
|
|
18
|
-
nextUrl:
|
|
27
|
+
nextUrl: resolveRedirectTarget(currentUrl, location),
|
|
19
28
|
};
|
|
20
29
|
}
|
|
21
30
|
function assertRedirectWithinLimit(response, currentUrl, redirectLimit, redirectCount) {
|
|
22
31
|
if (redirectCount < redirectLimit)
|
|
23
32
|
return;
|
|
24
|
-
|
|
33
|
+
cancelResponseBody(response);
|
|
25
34
|
throw new FetchError('Too many redirects', currentUrl);
|
|
26
35
|
}
|
|
27
36
|
function getRedirectLocation(response, currentUrl) {
|
|
28
37
|
const location = response.headers.get('location');
|
|
29
38
|
if (location)
|
|
30
39
|
return location;
|
|
31
|
-
|
|
40
|
+
cancelResponseBody(response);
|
|
32
41
|
throw new FetchError('Redirect response missing Location header', currentUrl);
|
|
33
42
|
}
|
|
34
43
|
function annotateRedirectError(error, url) {
|
|
35
|
-
if (!error
|
|
44
|
+
if (!isRecord(error))
|
|
36
45
|
return;
|
|
37
46
|
error.requestUrl = url;
|
|
38
47
|
}
|
|
39
|
-
|
|
48
|
+
function resolveRedirectTarget(baseUrl, location) {
|
|
40
49
|
if (!URL.canParse(location, baseUrl)) {
|
|
41
50
|
throw createErrorWithCode('Invalid redirect target', 'EBADREDIRECT');
|
|
42
51
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Readable, Writable } from 'node:stream';
|
|
2
|
-
import { pipeline } from 'node:stream/promises';
|
|
3
1
|
import { FetchError } from '../../errors/app-error.js';
|
|
4
2
|
function assertContentLengthWithinLimit(response, url, maxBytes) {
|
|
5
3
|
const contentLengthHeader = response.headers.get('content-length');
|
|
@@ -9,51 +7,98 @@ function assertContentLengthWithinLimit(response, url, maxBytes) {
|
|
|
9
7
|
if (Number.isNaN(contentLength) || contentLength <= maxBytes) {
|
|
10
8
|
return;
|
|
11
9
|
}
|
|
10
|
+
cancelResponseBody(response);
|
|
12
11
|
throw new FetchError(`Response exceeds maximum size of ${maxBytes} bytes`, url);
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (typeof chunk === 'string') {
|
|
20
|
-
return Buffer.from(chunk);
|
|
21
|
-
}
|
|
22
|
-
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
13
|
+
function createReadState() {
|
|
14
|
+
return {
|
|
15
|
+
decoder: new TextDecoder(),
|
|
16
|
+
parts: [],
|
|
17
|
+
total: 0,
|
|
23
18
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
}
|
|
20
|
+
function appendChunk(state, chunk, maxBytes, url) {
|
|
21
|
+
state.total += chunk.byteLength;
|
|
22
|
+
if (state.total > maxBytes) {
|
|
23
|
+
throw new FetchError(`Response exceeds maximum size of ${maxBytes} bytes`, url);
|
|
24
|
+
}
|
|
25
|
+
const decoded = state.decoder.decode(chunk, { stream: true });
|
|
26
|
+
if (decoded)
|
|
27
|
+
state.parts.push(decoded);
|
|
28
|
+
}
|
|
29
|
+
function finalizeRead(state) {
|
|
30
|
+
const decoded = state.decoder.decode();
|
|
31
|
+
if (decoded)
|
|
32
|
+
state.parts.push(decoded);
|
|
33
|
+
}
|
|
34
|
+
function createAbortError(url) {
|
|
35
|
+
return new FetchError('Request was aborted during response read', url, 499, {
|
|
36
|
+
reason: 'aborted',
|
|
39
37
|
});
|
|
38
|
+
}
|
|
39
|
+
function cancelResponseBody(response) {
|
|
40
|
+
const cancelPromise = response.body?.cancel();
|
|
41
|
+
if (cancelPromise) {
|
|
42
|
+
cancelPromise.catch(() => {
|
|
43
|
+
// Best-effort cancellation; ignore failures.
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function cancelReaderQuietly(reader) {
|
|
40
48
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
await reader.cancel();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore cancel errors; we're already failing this read.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function throwIfAborted(signal, url, reader) {
|
|
56
|
+
if (!signal?.aborted)
|
|
57
|
+
return;
|
|
58
|
+
await cancelReaderQuietly(reader);
|
|
59
|
+
throw createAbortError(url);
|
|
60
|
+
}
|
|
61
|
+
async function handleReadFailure(error, signal, url, reader) {
|
|
62
|
+
const aborted = signal?.aborted ?? false;
|
|
63
|
+
await cancelReaderQuietly(reader);
|
|
64
|
+
if (aborted) {
|
|
65
|
+
throw createAbortError(url);
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
async function readAllChunks(reader, state, url, maxBytes, signal) {
|
|
70
|
+
await throwIfAborted(signal, url, reader);
|
|
71
|
+
let result = await reader.read();
|
|
72
|
+
while (!result.done) {
|
|
73
|
+
appendChunk(state, result.value, maxBytes, url);
|
|
74
|
+
await throwIfAborted(signal, url, reader);
|
|
75
|
+
result = await reader.read();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readStreamWithLimit(stream, url, maxBytes, signal) {
|
|
79
|
+
const state = createReadState();
|
|
80
|
+
const reader = stream.getReader();
|
|
81
|
+
try {
|
|
82
|
+
await readAllChunks(reader, state, url, maxBytes, signal);
|
|
43
83
|
}
|
|
44
84
|
catch (error) {
|
|
45
|
-
|
|
46
|
-
throw new FetchError('Request was aborted during response read', url, 499, { reason: 'aborted' });
|
|
47
|
-
}
|
|
48
|
-
throw error;
|
|
85
|
+
await handleReadFailure(error, signal, url, reader);
|
|
49
86
|
}
|
|
50
|
-
|
|
87
|
+
finally {
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
finalizeRead(state);
|
|
91
|
+
return { text: state.parts.join(''), size: state.total };
|
|
51
92
|
}
|
|
52
93
|
export async function readResponseText(response, url, maxBytes, signal) {
|
|
53
94
|
assertContentLengthWithinLimit(response, url, maxBytes);
|
|
54
95
|
if (!response.body) {
|
|
55
96
|
const text = await response.text();
|
|
56
|
-
|
|
97
|
+
const size = Buffer.byteLength(text);
|
|
98
|
+
if (size > maxBytes) {
|
|
99
|
+
throw new FetchError(`Response exceeds maximum size of ${maxBytes} bytes`, url);
|
|
100
|
+
}
|
|
101
|
+
return { text, size };
|
|
57
102
|
}
|
|
58
103
|
return readStreamWithLimit(response.body, url, maxBytes, signal);
|
|
59
104
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import type { Dispatcher } from 'undici';
|
|
1
2
|
import type { FetchOptions } from '../config/types/runtime.js';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export declare const dispatcher: Dispatcher;
|
|
4
|
+
export declare function destroyAgents(): void;
|
|
5
|
+
interface FetchTelemetryContext {
|
|
6
|
+
requestId: string;
|
|
7
|
+
startTime: number;
|
|
8
|
+
url: string;
|
|
9
|
+
method: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function startFetchTelemetry(url: string, method: string): FetchTelemetryContext;
|
|
12
|
+
export declare function recordFetchResponse(context: FetchTelemetryContext, response: Response, contentSize?: number): void;
|
|
13
|
+
export declare function recordFetchError(context: FetchTelemetryContext, error: unknown, status?: number): void;
|
|
14
|
+
export declare function fetchWithRedirects(url: string, init: RequestInit, maxRedirects: number): Promise<{
|
|
15
|
+
response: Response;
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function readResponseText(response: Response, url: string, maxBytes: number, signal?: AbortSignal): Promise<{
|
|
19
|
+
text: string;
|
|
20
|
+
size: number;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function fetchNormalizedUrl(normalizedUrl: string, options?: FetchOptions): Promise<string>;
|
|
23
|
+
export {};
|