#11481·better-auth

oauth-provider: auth.api.oauth2Consent() always fails "request not found" without a raw Request

Author: adarshmadrechaCreated Sep 30, 2026Updated Sep 30, 2026

Is this suited for github?

  • Yes, this is suited for github

Reproduction

The @better-auth/mcp docs describe a custom consentPage. Completing that consent server-side via auth.api.oauth2Consent always fails, so a custom consent page can never accept a connection.

Minimal steps:

  1. Configure auth with jwt() + mcp() (a bare oauthProvider() shows the same) and consentPage: "/consent".
  2. Drive the authorize flow until the browser lands on the custom consent page (/consent?response_type=code&…&sig=…). Keep the signed query string and the session cookie.
  3. From a server route that has both, complete consent:
typescript
await auth.api.oauth2Consent({
  headers: new Headers({ cookie }),
  body: { accept: true, oauth_query: oauthQuery },
});
  1. Observe the throw:
json
{
  "error": "invalid_request",
  "error_description": "request not found"
}

accept: false works; only the accept path fails.

Current vs. Expected behavior

Current

  • auth.api.oauth2Consent({ headers, body }) throws "request not found". Passing headers (including the session cookie) does not help — the missing piece is ctx.request, not auth.
  • Every custom-consent accept is impossible through the auth.api surface.

Root cause:

typescript
if (!ctx.request) {
  throw new APIError('UNAUTHORIZED', {
    error_description: 'request not found',
    error: 'invalid_request',
  });
}
  • When the endpoint is invoked through auth.api.* instead of auth.handler(request), better-call builds the endpoint context without request, so this guard always trips. The consent handler already has everything the re-dispatched authorize needs (ctx.body.oauth_query is verified and ctx.query is set), so the guard is not required for this internal re-dispatch.

Expected

  • Completing consent via auth.api.oauth2Consent() should work from a server route with a valid session — or the API/types should require request explicitly and document it, instead of failing at runtime with a generic invalid_request.

What version of Better Auth are you using?

1.7.6 (also reproducible against main as of 2026-09-30; the guard and the consent re-dispatch are unchanged there).

System info

npx auth info --json is not runnable in this Bun app; versions from the lockfile:

bash
better-auth: 1.7.6
@better-auth/oauth-provider: 1.7.6
@better-auth/mcp: 1.7.6
runtime: Bun 1.4.2
os: Linux x86_64

Which area(s) are affected? (Select all that apply)

  • Backend
  • Package
  • Types

Auth config (if applicable)

typescript
import { betterAuth } from 'better-auth';
import { jwt } from 'better-auth/plugins';
import { cimd } from '@better-auth/cimd';
import { mcp } from '@better-auth/mcp';

export const auth = betterAuth({
  plugins: [
    jwt(),
    mcp({
      loginPage: '/login',
      consentPage: '/consent',
      resource: 'https://example.com/mcp',
    }),
    cimd({ fetchClientMetadataResource, metadataProfile: 'mcp-2026-07-28' }),
  ],
});

Additional context

Verified workarounds (both complete the consent and return the redirect carrying the authorization code):

  1. Pass a Request and asResponse: false:
typescript
const request = new Request('http://localhost:3000/api/auth/oauth2/consent', {
  method: 'POST',
  headers: { accept: 'application/json', 'content-type': 'application/json', cookie },
  body: JSON.stringify({ accept: true, oauth_query: oauthQuery }),
});

await auth.api.oauth2Consent({
  headers: request.headers,
  body: { accept: true, oauth_query: oauthQuery },
  request,
  asResponse: false,
});
  1. Or run the same endpoint through auth.handler(request) and read the JSON response.

Related: #8806 (auth.api.oauth2UserInfo → "request not found", closed/locked). The answer there — "pass headers explicitly" — does not apply here: headers are already passed, and the failing check is ctx.request.

Impact: the documented @better-auth/mcp custom-consent path is unusable. Every external MCP host (Claude Code, Cursor, custom clients) that goes through a custom consent page hits this on Approve.