{"record":{"id":"94373e95bc8d9acc","repo":"agalwood/Motrix","slug":"plugin-http-timeout","errorCode":"plugin.http.timeout","errorMessage":"Request timed out after ${timeoutMs}ms","messagePattern":"Request timed out after (.+?)ms","errorType":"exception","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/http.ts","lineNumber":334,"sourceCode":"            // different host.\n            delete reqHeaders.cookie\n          }\n        }\n\n        let response: Awaited<ReturnType<typeof undiciRequest>>\n        try {\n          response = await undiciRequest(currentUrl, {\n            method,\n            headers: reqHeaders,\n            body: bodyPayload,\n            signal: internalCtrl.signal,\n            dispatcher,\n          })\n        } catch (err: unknown) {\n          if (internalCtrl.signal.aborted) {\n            const reason = internalCtrl.signal.reason\n            if (reason === 'timeout') {\n              throw new HttpError(\n                'plugin.http.timeout',\n                `Request timed out after ${timeoutMs}ms`\n              )\n            }\n            if (reason === 'plugin_abort') {\n              throw new HttpError(\n                'plugin.http.aborted',\n                'Request aborted by plugin'\n              )\n            }\n            throw new HttpError('plugin.http.aborted', 'Request aborted')\n          }\n          if (\n            err instanceof Error &&\n            (err.name === 'AbortError' ||\n              err.name === 'DOMException' ||\n              err.constructor?.name === 'DOMException')\n          ) {","sourceCodeStart":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/http.ts#L316-L352","documentation":"A setTimeout timer fires after timeoutMs (clamped to between 1_000ms and 300_000ms) and aborts the internal controller with reason 'timeout'. The undici request rejects, the catch block matches the reason, and the timeout is surfaced as a typed HttpError rather than a generic AbortError. timeoutMs comes from opts.timeoutMs or the host default.","triggerScenarios":"Server response slower than timeoutMs; timeoutMs explicitly set too low; network stall mid-stream; large upload that cannot finish in the window.","commonSituations":"Default timeout too aggressive for slow endpoints; plugin does not override opts.timeoutMs and inherits a small host default; cold-start latency on a serverless target.","solutions":["Raise opts.timeoutMs up to the 300_000ms clamp ceiling where the endpoint legitimately needs it.","Move large transfers to a background job and poll, instead of blocking one HTTP call.","Retry with exponential backoff for transient slowness.","Optimize the server endpoint's p99 latency."],"exampleFix":"// before\nawait http.request({ url, responseType: 'json', timeoutMs: 1000 })\n\n// after\nawait http.request({ url, responseType: 'json', timeoutMs: 30000 })","handlingStrategy":"retry","validationCode":"const timeoutMs = Math.min(300_000, Math.max(1_000, expectedLatencyMs * 3))\nawait http.request({ url, responseType: 'json', timeoutMs })","typeGuard":null,"tryCatchPattern":"for (const attempt of [1,2,3]) {\n  try {\n    return await http.request({ url, responseType: 'json', timeoutMs })\n  } catch (e) {\n    if (e instanceof HttpError && e.code === 'plugin.http.timeout' && attempt < 3) {\n      await sleep(2 ** attempt * 200); continue\n    }\n    throw e\n  }\n}","preventionTips":["Set opts.timeoutMs based on the endpoint's known p99 latency, not a guess.","Surface timeout as a retryable condition in your error layer.","Move transfers longer than 300s (the clamp ceiling) to a background job + polling pattern."],"tags":["http","timeout","network","performance"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}