{"record":{"id":"dc7b37c1ba24a87a","repo":"agalwood/Motrix","slug":"plugin-http-redirect-not-allowed","errorCode":"plugin.http.redirect_not_allowed","errorMessage":"redirect: 'error' set; refusing to follow ${status} to ${location}","messagePattern":"redirect: 'error' set; refusing to follow (.+?) to (.+?)","errorType":"exception","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/http.ts","lineNumber":396,"sourceCode":"        const location = response.headers.location\n        const isRedirect = status >= 300 && status < 400 && location\n\n        if (isRedirect) {\n          if (redirect === 'manual') {\n            // Surface the 3xx as-is.\n            return await buildResponse<R>(\n              response,\n              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.","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/http.ts#L378-L414","documentation":"The caller explicitly set redirect:'error', and the server returned a 3xx with a Location header. The body is dumped (drained) and the request fails instead of following or returning the redirect. This is the strict opt-in mode for plugins that want to forbid silent redirects (e.g. to avoid SSRF or auth stripping).","triggerScenarios":"Passing redirect:'error' and hitting any URL that returns 301/302/303/307/308; an endpoint that always redirects (e.g. http->https, trailing-slash normalization); a short-link service.","commonSituations":"Security-hardened plugin that disallows redirects; misconfigured base URL pointing at a redirecting endpoint; ops changed a URL to redirect.","solutions":["Switch to redirect:'follow' (the default) if following is acceptable.","Switch to redirect:'manual' to inspect the 3xx and the Location yourself.","Fix the upstream URL so it resolves directly without a redirect."],"exampleFix":"// before\nawait http.request({ url, responseType: 'json', redirect: 'error' })\n\n// after\nawait http.request({ url, responseType: 'json', redirect: 'follow' })","handlingStrategy":"validation","validationCode":"const redirect = expectRedirect ? 'follow' : 'manual'\nawait http.request({ ...opts, redirect })","typeGuard":null,"tryCatchPattern":"try {\n  await http.request({ ...opts, redirect: 'error' })\n} catch (e) {\n  if (e instanceof HttpError && e.code === 'plugin.http.redirect_not_allowed') {\n    // expected; switch to 'manual' or 'follow' depending on policy\n  } else throw e\n}","preventionTips":["Default to 'follow' unless you have an explicit security reason to forbid redirects.","Use 'manual' when you need to inspect the 3xx and the Location yourself.","Resolve redirecting URLs at the base URL once, rather than per request."],"tags":["http","redirect","security","configuration"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}