{"record":{"id":"be6ae8778a8e7473","repo":"mastra-ai/mastra","slug":"mcp-client-redirect-body-not-replayable","errorCode":"MCP_CLIENT_REDIRECT_BODY_NOT_REPLAYABLE","errorMessage":"Cannot follow a ${response.status} redirect under the allowedHosts policy: the request body is not replayable (only string bodies can be re-sent).","messagePattern":"Cannot follow a (.+?) redirect under the allowedHosts policy: the request body is not replayable \\(only string bodies can be re-sent\\)\\.","errorType":"error_code","errorClass":"MastraError","httpStatus":null,"severity":"error","filePath":"packages/mcp/src/client/url-policy.ts","lineNumber":242,"sourceCode":"    // Release the redirect response's body so its socket can be reused.\n    cancelResponseBody(response);\n\n    const nextUrl = new URL(location, currentUrl);\n    assertHostAllowed(nextUrl, allowedHosts, `A redirect from \"${currentUrl.host}\" pointed at it; the hop was not followed.`);\n\n    const methodUpper = method.toUpperCase();\n    const dropsBody =\n      response.status === 303 || ((response.status === 301 || response.status === 302) && methodUpper === 'POST');\n    if (dropsBody) {\n      method = 'GET';\n      body = undefined;\n      headers.delete('content-type');\n      headers.delete('content-length');\n    } else if (body != null && typeof body !== 'string') {\n      // Any hop that preserves the body (307/308 always; 301/302 for non-POST\n      // methods) would re-send an already consumed one-shot body, so guard on\n      // \"the body is preserved\", not on specific status codes.\n      throw new MastraError({\n        id: 'MCP_CLIENT_REDIRECT_BODY_NOT_REPLAYABLE',\n        domain: ErrorDomain.MCP,\n        category: ErrorCategory.USER,\n        text: `Cannot follow a ${response.status} redirect under the allowedHosts policy: the request body is not replayable (only string bodies can be re-sent).`,\n      });\n    }\n\n    // WHATWG Fetch strips Authorization when the ORIGIN (scheme + host + port)\n    // changes — host alone is not enough: a same-host https→http downgrade must\n    // also drop the header or the bearer token is re-sent in cleartext.\n    if (nextUrl.origin !== currentUrl.origin) {\n      headers.delete('authorization');\n    }\n\n    currentUrl = nextUrl;\n  }\n}\n","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/mcp/src/client/url-policy.ts#L224-L260","documentation":"Redirects that preserve the body (307/308, or 301/302 for non-POST methods) require resending the request body. Under the allowedHosts policy, if the current body is neither null nor a string (e.g. a stream, FormData, or an already-consumed one-shot body), following the redirect would re-send an unusable body, so MCP_CLIENT_REDIRECT_BODY_NOT_REPLAYABLE (USER category) is thrown.","triggerScenarios":"Sending a request with a non-string body (stream/Buffer/FormData) to a host that answers 307/308 redirect under an allowedHosts URL policy.","commonSituations":"Posting binary uploads or streaming JSON-RPC bodies to an MCP endpoint that now redirects (e.g. trailing-slash or domain migration); ReadableStream bodies consumed on the first hop.","solutions":["Send the request body as a string (e.g. JSON.stringify(payload)) so it can be replayed across hops","Target the post-redirect URL directly to avoid the redirect hop","Buffer the body into a string before fetching","Restructure the server so the endpoint does not redirect for body-bearing requests"],"exampleFix":"// before\nawait fetch(url, { method: 'POST', body: fs.createReadStream(path) });\n// after\nawait fetch(url, { method: 'POST', body: JSON.stringify(payload), headers: { 'content-type': 'application/json' } });","handlingStrategy":"validation","validationCode":"if (body != null && typeof body !== 'string') {\n  throw new Error('Body must be a string (JSON.stringify it) to survive 307/308 redirects under allowedHosts policy');\n}","typeGuard":"function isReplayableBody(b: unknown): b is string | null | undefined {\n  return b == null || typeof b === 'string';\n}","tryCatchPattern":"try {\n  await client.tools();\n} catch (e) {\n  if (e instanceof MastraError && e.id === 'MCP_CLIENT_REDIRECT_BODY_NOT_REPLAYABLE') {\n    console.error('Re-send with a stringified body or target the post-redirect URL directly');\n  } else throw e;\n}","preventionTips":["Always send JSON-stringified bodies to MCP endpoints","Avoid streaming/FormData bodies when redirects are possible","Resolve final URLs up front so no body-bearing redirect occurs"],"tags":["mcp","redirect","request-body","fetch","url-policy"],"backgroundTag":"redirect-body-not-replayable","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}