{"record":{"id":"cc121866cb8a1c84","repo":"agalwood/Motrix","slug":"plugin-http-too-many-redirects","errorCode":"plugin.http.too_many_redirects","errorMessage":"Too many redirects (>${MAX_REDIRECTS})","messagePattern":"Too many redirects \\(>(.+?)\\)","errorType":"exception","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/http.ts","lineNumber":404,"sourceCode":"              opts.responseType,\n              maxBodyBytes,\n              internalCtrl,\n              currentUrl,\n              redirected,\n              doCleanup\n            )\n          }\n          if (redirect === 'error') {\n            await response.body.dump?.()\n            throw new HttpError(\n              'plugin.http.redirect_not_allowed',\n              `redirect: 'error' set; refusing to follow ${status} to ${location}`\n            )\n          }\n          // redirect === 'follow'\n          if (hops >= MAX_REDIRECTS) {\n            await response.body.dump?.()\n            throw new HttpError(\n              'plugin.http.too_many_redirects',\n              `Too many redirects (>${MAX_REDIRECTS})`\n            )\n          }\n          await response.body.dump?.()\n          const loc = Array.isArray(location) ? (location[0] ?? '') : location\n          const nextUrl = new URL(loc, currentUrl)\n          // Re-validate the scheme on every hop. checkScheme only ran on the\n          // initial URL, so a 3xx Location to file:// (or any non-http scheme)\n          // would otherwise escape the allowlist.\n          checkScheme(nextUrl)\n          currentUrl = nextUrl.toString()\n          redirected = true\n          hops += 1\n          continue\n        }\n\n        return await buildResponse<R>(","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/http.ts#L386-L422","documentation":"With redirect:'follow', each 3xx hop increments a counter; when it reaches MAX_REDIRECTS (10) the body is dumped and the request fails. This bounds redirect chains to prevent loops and resource exhaustion.","triggerScenarios":"A redirect loop (A->B->A); an unusually long legitimate chain (>10 hops); a misconfigured server whose Location points back to itself; cookie/auth loss causing repeated re-auth redirects.","commonSituations":"Server redirect loop bug; CDN cascading redirects; login flow that bounces between auth providers; URL shortener chains.","solutions":["Switch to redirect:'manual' and walk hops explicitly with a visited-set to detect loops.","Fix the upstream redirect loop (server config, auth cookie scope, trailing-slash rules).","Use the final, already-resolved URL as the request target.","Set redirect:'error' to fail fast on the first hop if redirects are not expected."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await http.request({ ...opts, redirect: 'follow' })\n} catch (e) {\n  if (e instanceof HttpError && e.code === 'plugin.http.too_many_redirects') {\n    // fall back to manual hop-walking with a visited-set to detect the loop\n    return await followManually(opts.url)\n  }\n  throw e\n}","preventionTips":["When following redirects, walk hops manually with a visited-URL set to detect loops fast.","Investigate any change from N hops to >10 hops - it usually indicates a server-side regression.","Pin final URLs at the call site when you know them, to avoid redirect chains entirely."],"tags":["http","redirect","loop","safety"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}