{"record":{"id":"a4ad857eebf5ed3d","repo":"agalwood/Motrix","slug":"plugin-http-aborted","errorCode":"plugin.http.aborted","errorMessage":"Request aborted by plugin","messagePattern":"Request aborted by plugin","errorType":"exception","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/http.ts","lineNumber":286,"sourceCode":"    }\n\n    // ---- Request body ----\n    let bodyPayload: string | Uint8Array | undefined\n    if (opts.body) {\n      const { bodyStr, contentType } = buildBodyPayload(opts.body)\n      bodyPayload = bodyStr\n      if (contentType && !reqHeaders['content-type']) {\n        reqHeaders['content-type'] = contentType\n      }\n    }\n\n    // ---- Abort plumbing ----\n    const internalCtrl = new AbortController()\n    const cleanup: (() => void)[] = []\n\n    if (opts.signal) {\n      if (opts.signal.aborted) {\n        throw new HttpError('plugin.http.aborted', 'Request aborted by plugin')\n      }\n      const onPluginAbort = () => internalCtrl.abort('plugin_abort')\n      opts.signal.addEventListener('abort', onPluginAbort, { once: true })\n      cleanup.push(() =>\n        opts.signal?.removeEventListener('abort', onPluginAbort)\n      )\n    }\n\n    const timer = setTimeout(() => internalCtrl.abort('timeout'), timeoutMs)\n    cleanup.push(() => clearTimeout(timer))\n\n    const doCleanup = () => {\n      for (const fn of cleanup) fn()\n    }\n\n    // ---- Per-hop loop ----\n    let currentUrl = opts.url\n    let redirected = false","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/http.ts#L268-L304","documentation":"Before opening the undici request, the capability inspects opts.signal.aborted. If the plugin-supplied signal is already aborted it throws immediately, avoiding a wasted in-flight request and a confusing late abort. This is a preflight guard, distinct from the mid-flight aborts in errors 104-106.","triggerScenarios":"Passing an AbortSignal that was aborted before request() was called; reusing a signal from an already-cancelled task; a race where an outer controller aborts just before the call.","commonSituations":"Plugin's parent task was cancelled and the signal propagated down; user double-clicked cancel; signal stored on a long-lived object and reused after a prior abort.","solutions":["Check signal.aborted before calling request() and short-circuit gracefully.","Pass a fresh AbortController per logical request instead of reusing a consumed signal.","If the abort is expected, treat this error as a normal cancellation outcome, not a fault."],"exampleFix":"// before\ncontroller.abort()\nawait http.request({ url, responseType: 'json', signal: controller.signal })\n\n// after\nif (controller.signal.aborted) return null\nawait http.request({ url, responseType: 'json', signal: controller.signal })","handlingStrategy":"validation","validationCode":"if (opts.signal?.aborted) {\n  // already cancelled; do not call request()\n  return undefined\n}\nawait http.request(opts)","typeGuard":"function isSignalFresh(signal: AbortSignal | undefined): boolean {\n  return !signal?.aborted\n}","tryCatchPattern":"try {\n  await http.request(opts)\n} catch (e) {\n  if (e instanceof HttpError && e.code === 'plugin.http.aborted' && /aborted by plugin/.test(e.message)) {\n    // pre-flight abort; expected during cancellation, exit gracefully\n  } else throw e\n}","preventionTips":["Create a new AbortController per logical request; do not reuse consumed signals.","Check signal.aborted at the top of any function that drives an HTTP call.","Propagate cancellation as a normal return value, not an exception, in your plugin's API."],"tags":["http","abort","race-condition","cancellation"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}