DocumentsBetter Auth
Request Body Cloning
Request Body Cloning
Type
Topic
Status
Published
Created
Jul 8, 2026
Updated
Jul 8, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Request Body Cloning#

Overview#

HTTP request bodies are one-time readable streams. Once a framework's body parser consumes the body (to deserialize JSON or form data into ctx.body), any subsequent attempt to read ctx.request — including by user-supplied callbacks — raises:

TypeError: Body is unusable: Body has already been read

Better-auth solves this with two complementary mechanisms:

  1. cloneRequest: true — an endpoint-level option (from the better-call library) that instructs the routing infrastructure to clone the raw Request object before the body is parsed, preserving a readable copy in ctx.request.
  2. ctx.request?.clone() — called at every callsite where the request is forwarded to a user callback, creating an additional independent copy so each async consumer gets its own unread stream.

This pattern was systematically introduced in PR #9619 to fix issue #8969.


Affected Endpoints#

EndpointFilecloneRequestCallbacks receiving clone()
POST /sign-up/emailsign-up.tsonExistingUserSignUp , sendVerificationEmail
POST /sign-in/emailsign-in.tssendVerificationEmail
POST /send-verification-emailemail-verification.tssendVerificationEmail via sendVerificationEmailFn
GET /verify-emailemail-verification.tssendVerificationEmail

Note: GET /verify-email has no body to parse, so cloneRequest is not set — but it still calls ctx.request?.clone() when invoking callbacks, following the same defensive pattern.


How It Works#

Step 1 — Clone at endpoint definition#

Declaring cloneRequest: true in the endpoint options tells better-call to clone the Request before handing it to the body parser. The original request stays accessible as ctx.request with an unread body.

// sign-up.ts (simplified)
createAuthEndpoint("/sign-up/email", {
  method: "POST",
  cloneRequest: true, // ← preserve readable ctx.request
  body: signUpEmailBodySchema,
  ...
}, async (ctx) => { ... });

Step 2 — Clone again per callback#

Each user callback receives its own clone, so multiple callbacks in one request lifecycle don't race over the same stream:

ctx.context.options.emailVerification.sendVerificationEmail(
  { user, url, token },
  ctx.request?.clone(), // ← fresh clone per call
);

The same pattern applies in sign-in.ts and email-verification.ts .


Where sendVerificationEmailFn Differs#

The shared helper sendVerificationEmailFn — called when the session already exists — passes ctx.request directly (not cloned) because it is awaited synchronously and is the only consumer of the request at that point . Callsites that call it from endpoints with cloneRequest: true still benefit from the cloned ctx.request.


Key Source References#