@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.
Files changed (173) hide show
  1. package/README.md +131 -156
  2. package/dist/config/auth-config.d.ts +16 -0
  3. package/dist/config/auth-config.js +53 -0
  4. package/dist/config/constants.d.ts +11 -13
  5. package/dist/config/constants.js +1 -3
  6. package/dist/config/env-parsers.d.ts +7 -0
  7. package/dist/config/env-parsers.js +84 -0
  8. package/dist/config/formatting.d.ts +2 -2
  9. package/dist/config/index.d.ts +47 -53
  10. package/dist/config/index.js +35 -64
  11. package/dist/config/types/content.d.ts +1 -49
  12. package/dist/config/types/runtime.d.ts +8 -16
  13. package/dist/config/types/tools.d.ts +2 -28
  14. package/dist/http/accept-policy.d.ts +3 -0
  15. package/dist/http/accept-policy.js +45 -0
  16. package/dist/http/async-handler.d.ts +2 -0
  17. package/dist/http/async-handler.js +5 -0
  18. package/dist/http/auth-introspection.d.ts +2 -0
  19. package/dist/http/auth-introspection.js +141 -0
  20. package/dist/http/auth-static.d.ts +2 -0
  21. package/dist/http/auth-static.js +23 -0
  22. package/dist/http/auth.d.ts +3 -2
  23. package/dist/http/auth.js +254 -23
  24. package/dist/http/cors.d.ts +6 -6
  25. package/dist/http/cors.js +7 -42
  26. package/dist/http/download-routes.d.ts +0 -12
  27. package/dist/http/download-routes.js +21 -58
  28. package/dist/http/host-allowlist.d.ts +3 -0
  29. package/dist/http/host-allowlist.js +117 -0
  30. package/dist/http/jsonrpc-http.d.ts +2 -0
  31. package/dist/http/jsonrpc-http.js +10 -0
  32. package/dist/http/mcp-routes.d.ts +8 -3
  33. package/dist/http/mcp-routes.js +137 -31
  34. package/dist/http/mcp-session-eviction.d.ts +3 -0
  35. package/dist/http/mcp-session-eviction.js +24 -0
  36. package/dist/http/mcp-session-helpers.d.ts +0 -1
  37. package/dist/http/mcp-session-helpers.js +1 -1
  38. package/dist/http/mcp-session-init.d.ts +7 -0
  39. package/dist/http/mcp-session-init.js +94 -0
  40. package/dist/http/mcp-session-slots.d.ts +17 -0
  41. package/dist/http/mcp-session-slots.js +55 -0
  42. package/dist/http/mcp-session-transport-init.d.ts +7 -0
  43. package/dist/http/mcp-session-transport-init.js +41 -0
  44. package/dist/http/mcp-session-transport.d.ts +7 -0
  45. package/dist/http/mcp-session-transport.js +57 -0
  46. package/dist/http/mcp-session-types.d.ts +5 -0
  47. package/dist/http/mcp-session-types.js +1 -0
  48. package/dist/http/mcp-session.d.ts +9 -9
  49. package/dist/http/mcp-session.js +15 -137
  50. package/dist/http/mcp-sessions.d.ts +43 -0
  51. package/dist/http/mcp-sessions.js +392 -0
  52. package/dist/http/mcp-validation.d.ts +1 -0
  53. package/dist/http/mcp-validation.js +11 -10
  54. package/dist/http/protocol-policy.d.ts +2 -0
  55. package/dist/http/protocol-policy.js +31 -0
  56. package/dist/http/rate-limit.js +7 -4
  57. package/dist/http/server-config.d.ts +1 -0
  58. package/dist/http/server-config.js +40 -0
  59. package/dist/http/server-middleware.d.ts +7 -9
  60. package/dist/http/server-middleware.js +9 -70
  61. package/dist/http/server-shutdown.d.ts +4 -0
  62. package/dist/http/server-shutdown.js +43 -0
  63. package/dist/http/server.d.ts +10 -0
  64. package/dist/http/server.js +546 -61
  65. package/dist/http/session-cleanup.js +8 -5
  66. package/dist/middleware/error-handler.d.ts +1 -1
  67. package/dist/middleware/error-handler.js +32 -33
  68. package/dist/resources/cached-content-params.d.ts +5 -0
  69. package/dist/resources/cached-content-params.js +36 -0
  70. package/dist/resources/cached-content.js +67 -125
  71. package/dist/resources/index.js +0 -82
  72. package/dist/server.js +50 -29
  73. package/dist/services/cache-events.d.ts +8 -0
  74. package/dist/services/cache-events.js +19 -0
  75. package/dist/services/cache-keys.d.ts +7 -0
  76. package/dist/services/cache-keys.js +57 -0
  77. package/dist/services/cache.d.ts +4 -9
  78. package/dist/services/cache.js +77 -139
  79. package/dist/services/context.d.ts +0 -1
  80. package/dist/services/context.js +0 -7
  81. package/dist/services/extractor.js +55 -116
  82. package/dist/services/fetcher/agents.d.ts +2 -2
  83. package/dist/services/fetcher/agents.js +35 -96
  84. package/dist/services/fetcher/dns-selection.d.ts +2 -0
  85. package/dist/services/fetcher/dns-selection.js +72 -0
  86. package/dist/services/fetcher/interceptors.d.ts +0 -22
  87. package/dist/services/fetcher/interceptors.js +18 -32
  88. package/dist/services/fetcher/redirects.js +16 -7
  89. package/dist/services/fetcher/response.js +79 -34
  90. package/dist/services/fetcher.d.ts +22 -3
  91. package/dist/services/fetcher.js +544 -44
  92. package/dist/services/fifo-queue.d.ts +8 -0
  93. package/dist/services/fifo-queue.js +25 -0
  94. package/dist/services/logger.js +2 -2
  95. package/dist/services/metadata-collector.d.ts +1 -9
  96. package/dist/services/metadata-collector.js +71 -2
  97. package/dist/services/transform-worker-pool.d.ts +4 -14
  98. package/dist/services/transform-worker-pool.js +177 -129
  99. package/dist/services/transform-worker-types.d.ts +32 -0
  100. package/dist/services/transform-worker-types.js +14 -0
  101. package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
  102. package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
  103. package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
  104. package/dist/tools/handlers/fetch-single.shared.js +175 -89
  105. package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
  106. package/dist/tools/handlers/fetch-url.tool.js +84 -119
  107. package/dist/tools/index.js +21 -40
  108. package/dist/tools/schemas.d.ts +1 -51
  109. package/dist/tools/schemas.js +1 -107
  110. package/dist/tools/utils/cached-markdown.d.ts +5 -0
  111. package/dist/tools/utils/cached-markdown.js +46 -0
  112. package/dist/tools/utils/content-shaping.d.ts +4 -0
  113. package/dist/tools/utils/content-shaping.js +67 -0
  114. package/dist/tools/utils/content-transform.d.ts +5 -17
  115. package/dist/tools/utils/content-transform.js +134 -114
  116. package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
  117. package/dist/tools/utils/fetch-pipeline.js +57 -63
  118. package/dist/tools/utils/frontmatter.d.ts +3 -0
  119. package/dist/tools/utils/frontmatter.js +73 -0
  120. package/dist/tools/utils/inline-content.d.ts +1 -2
  121. package/dist/tools/utils/inline-content.js +4 -7
  122. package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
  123. package/dist/tools/utils/markdown-heuristics.js +19 -0
  124. package/dist/tools/utils/markdown-signals.d.ts +1 -0
  125. package/dist/tools/utils/markdown-signals.js +19 -0
  126. package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
  127. package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
  128. package/dist/tools/utils/raw-markdown.d.ts +6 -0
  129. package/dist/tools/utils/raw-markdown.js +135 -0
  130. package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
  131. package/dist/transformers/markdown/fenced-code-rule.js +38 -0
  132. package/dist/transformers/markdown/frontmatter.d.ts +2 -0
  133. package/dist/transformers/markdown/frontmatter.js +45 -0
  134. package/dist/transformers/markdown/noise-rule.d.ts +2 -0
  135. package/dist/transformers/markdown/noise-rule.js +80 -0
  136. package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
  137. package/dist/transformers/markdown/turndown-instance.js +19 -0
  138. package/dist/transformers/markdown.d.ts +2 -0
  139. package/dist/transformers/markdown.js +185 -0
  140. package/dist/transformers/markdown.transformer.js +5 -117
  141. package/dist/utils/cached-payload.d.ts +7 -0
  142. package/dist/utils/cached-payload.js +36 -0
  143. package/dist/utils/code-language-bash.d.ts +1 -0
  144. package/dist/utils/code-language-bash.js +48 -0
  145. package/dist/utils/code-language-core.d.ts +2 -0
  146. package/dist/utils/code-language-core.js +13 -0
  147. package/dist/utils/code-language-detectors.d.ts +5 -0
  148. package/dist/utils/code-language-detectors.js +142 -0
  149. package/dist/utils/code-language-helpers.d.ts +5 -0
  150. package/dist/utils/code-language-helpers.js +62 -0
  151. package/dist/utils/code-language-parsing.d.ts +5 -0
  152. package/dist/utils/code-language-parsing.js +62 -0
  153. package/dist/utils/code-language.d.ts +9 -0
  154. package/dist/utils/code-language.js +250 -46
  155. package/dist/utils/error-details.d.ts +3 -0
  156. package/dist/utils/error-details.js +12 -0
  157. package/dist/utils/error-utils.js +1 -1
  158. package/dist/utils/filename-generator.js +34 -12
  159. package/dist/utils/guards.d.ts +1 -0
  160. package/dist/utils/guards.js +3 -0
  161. package/dist/utils/header-normalizer.d.ts +0 -3
  162. package/dist/utils/header-normalizer.js +3 -3
  163. package/dist/utils/ip-address.d.ts +4 -0
  164. package/dist/utils/ip-address.js +6 -0
  165. package/dist/utils/tool-error-handler.d.ts +2 -2
  166. package/dist/utils/tool-error-handler.js +14 -46
  167. package/dist/utils/url-transformer.d.ts +7 -0
  168. package/dist/utils/url-transformer.js +147 -0
  169. package/dist/utils/url-validator.d.ts +1 -2
  170. package/dist/utils/url-validator.js +53 -114
  171. package/dist/workers/content-transform.worker.d.ts +1 -0
  172. package/dist/workers/content-transform.worker.js +40 -0
  173. 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-utils.js';
5
- import { isBlockedIp } from '../../utils/url-validator.js';
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
- let done = false;
11
- const timer = setTimeout(() => {
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
- const legacy = options.verbatim;
50
- return typeof legacy === 'boolean' ? legacy : undefined;
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
- const options = {
54
- ...normalizedOptions,
55
- order: resolveResultOrder(normalizedOptions),
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 normalizeLookupResults(addresses, family) {
74
- if (Array.isArray(addresses)) {
75
- return addresses;
76
- }
77
- return [{ address: addresses, family: family ?? 4 }];
78
- }
79
- function handleLookupResult(error, addresses, hostname, resolvedFamily, useAll, callback) {
80
- if (error) {
81
- callback(error, addresses);
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
- address: first.address,
123
- family: first.family,
124
- fallback: list,
73
+ isDone: () => done,
74
+ markDone: () => {
75
+ done = true;
76
+ clearTimeout(timer);
77
+ },
125
78
  };
126
79
  }
127
- function findBlockedIpError(list, hostname) {
128
- for (const addr of list) {
129
- const ip = typeof addr === 'string' ? addr : addr.address;
130
- if (!isBlockedIp(ip)) {
131
- continue;
132
- }
133
- return createErrorWithCode(`Blocked IP detected for ${hostname}`, 'EBLOCKED');
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-utils.js';
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
- publishFetchEnd(context, response.status, duration);
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
- if (contentType !== null) {
79
- meta.contentType = contentType;
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: `${Math.round(duration)}ms`,
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-utils.js';
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
- void response.body?.cancel();
24
+ cancelResponseBody(response);
16
25
  return {
17
26
  response,
18
- nextUrl: await resolveRedirectTarget(currentUrl, location),
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
- void response.body?.cancel();
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
- void response.body?.cancel();
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 || typeof error !== 'object')
44
+ if (!isRecord(error))
36
45
  return;
37
46
  error.requestUrl = url;
38
47
  }
39
- async function resolveRedirectTarget(baseUrl, location) {
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
- async function readStreamWithLimit(stream, url, maxBytes, signal) {
15
- const decoder = new TextDecoder();
16
- let total = 0;
17
- let text = '';
18
- const toBuffer = (chunk) => {
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
- const sink = new Writable({
25
- write(chunk, _encoding, callback) {
26
- const buffer = toBuffer(chunk);
27
- total += buffer.length;
28
- if (total > maxBytes) {
29
- callback(new FetchError(`Response exceeds maximum size of ${maxBytes} bytes`, url));
30
- return;
31
- }
32
- text += decoder.decode(buffer, { stream: true });
33
- callback();
34
- },
35
- final(callback) {
36
- text += decoder.decode();
37
- callback();
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
- const readable = Readable.fromWeb(stream, { signal });
42
- await pipeline(readable, sink, { signal });
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
- if (signal?.aborted) {
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
- return { text, size: total };
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
- return { text, size: Buffer.byteLength(text) };
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
- import { destroyAgents } from './fetcher/agents.js';
3
- export { destroyAgents };
4
- export declare function fetchNormalizedUrlWithRetry(normalizedUrl: string, options?: FetchOptions, maxRetries?: number): Promise<string>;
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 {};