瀏覽代碼

fix: bound local fetch timer config

Tianyi Cui 2 月之前
父節點
當前提交
3f676efbd9

+ 2 - 2
docs/config-catalog.md

@@ -1059,7 +1059,7 @@ export interface Config {
   maxResponseBytes?: number
   /** Maximum decoded body length in characters. */
   maxBodyChars?: number
-  /** Default fetch timeout in milliseconds. */
+  /** Default fetch timeout in milliseconds, within Node's timer range. */
   timeoutMs?: number
   /** Maximum number of same-origin redirect hops to follow. */
   maxRedirects?: number
@@ -1068,7 +1068,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/web/web-fetch-local/src/index.ts:34`](../packages/web/web-fetch-local/src/index.ts)
+Source: [`packages/web/web-fetch-local/src/index.ts:36`](../packages/web/web-fetch-local/src/index.ts)
 
 ## `@deepseek-ai/dsh-web-search-deepseek`
 

+ 1 - 1
packages/web/web-fetch-local/README.md

@@ -26,7 +26,7 @@ The provider's configured `timeoutMs` is a **resource backstop** for direct `ctx
 | `maxUrlLength` | `2048` | Maximum accepted request URL length. |
 | `maxResponseBytes` | `5_000_000` | Maximum response body size in bytes. |
 | `maxBodyChars` | `100_000` | Maximum decoded body length in characters. |
-| `timeoutMs` | `30_000` | Fetch timeout — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). |
+| `timeoutMs` | `30_000` | Fetch timeout within Node's timer range — a resource backstop for direct `ctx.web.fetch()` callers, not the model-facing tool-call budget (that is `dsh-timeout-policy`). |
 | `maxRedirects` | `5` | Maximum same-origin redirect hops (`0` follows none). |
 | `userAgent` | `deepseek-harness/…` | `User-Agent` header. |
 

+ 12 - 2
packages/web/web-fetch-local/src/index.ts

@@ -13,6 +13,8 @@ import type {} from '@deepseek-ai/dsh-web'
 import { LocalFetchProvider } from './provider.ts'
 import type { LocalFetchLimits } from './provider.ts'
 
+const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647
+
 export {
   LOCAL_FETCH_PROVIDER_ID,
   LocalFetchProvider,
@@ -38,7 +40,7 @@ export interface Config {
   maxResponseBytes?: number
   /** Maximum decoded body length in characters. */
   maxBodyChars?: number
-  /** Default fetch timeout in milliseconds. */
+  /** Default fetch timeout in milliseconds, within Node's timer range. */
   timeoutMs?: number
   /** Maximum number of same-origin redirect hops to follow. */
   maxRedirects?: number
@@ -65,6 +67,14 @@ function assertPositiveFinite(name: string, value: number): void {
   }
 }
 
+/** Node coerces larger timer delays to 1 ms, so reject them at configuration time. */
+function assertTimeoutMs(value: number): void {
+  assertPositiveFinite('timeoutMs', value)
+  if (value > MAX_NODE_TIMER_DELAY_MS) {
+    throw new Error(`web-fetch-local: timeoutMs must be no greater than ${MAX_NODE_TIMER_DELAY_MS}`)
+  }
+}
+
 /** The redirect hop cap must be a non-negative integer (0 follows no redirects). */
 function assertNonNegativeInteger(name: string, value: number): void {
   if (!Number.isInteger(value) || value < 0) {
@@ -79,7 +89,7 @@ export function apply(ctx: Context, config: Config): void {
   assertPositiveFinite('maxUrlLength', resolved.maxUrlLength)
   assertPositiveFinite('maxResponseBytes', resolved.maxResponseBytes)
   assertPositiveFinite('maxBodyChars', resolved.maxBodyChars)
-  assertPositiveFinite('timeoutMs', resolved.timeoutMs)
+  assertTimeoutMs(resolved.timeoutMs)
   assertNonNegativeInteger('maxRedirects', resolved.maxRedirects)
   const limits: LocalFetchLimits = {
     maxUrlLength: resolved.maxUrlLength,

+ 7 - 0
packages/web/web-fetch-local/tests/fetch-local.spec.ts

@@ -396,6 +396,13 @@ describe('web-fetch-local plugin registration', () => {
       .rejects.toThrow(/timeoutMs must be a positive finite number/)
   })
 
+  it('rejects a timeout beyond Node timer range at construction', async () => {
+    const ctx = new Context()
+    await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })
+    await expect(ctx.plugin(fetchPlugin, { timeoutMs: 2_147_483_648 }))
+      .rejects.toThrow(/timeoutMs must be no greater than 2147483647/)
+  })
+
   it('rejects a fractional redirect cap at construction', async () => {
     const ctx = new Context()
     await ctx.plugin(WebService, { fetchProvider: LOCAL_FETCH_PROVIDER_ID })