mastra-ai/mastra · error

Invalid email or password

Error message

Invalid email or password

What it means

After a sign-in request that returned OK, signIn() checks that the parsed JSON body contains result.user. If Better Auth returned a 2xx response without a user object, the provider throws 'Invalid email or password'. This is a defensive fallback for malformed or empty success responses.

Source

Thrown at auth/better-auth/src/index.ts:765

  async signIn(email: string, password: string, request: Request): Promise<CredentialsResult<EEUser>> {
    const headers = request?.headers ?? new Headers();

    // Use asResponse: true to get the full response with Set-Cookie headers
    const response = await this.auth.api.signInEmail({
      body: { email, password },
      headers,
      asResponse: true,
    });

    if (!response.ok) {
      const errorData = (await response.json().catch(() => ({}))) as { message?: string };
      throw new Error(errorData.message || 'Invalid email or password');
    }

    const result = (await response.json()) as { user?: User; token?: string | null };

    if (!result?.user) {
      throw new Error('Invalid email or password');
    }

    // Extract Set-Cookie headers from Better Auth response
    const cookies: string[] = [];
    const setCookieHeader = response.headers.get('set-cookie');
    if (setCookieHeader) {
      // Split multiple cookies (they may be comma-separated or in multiple headers)
      cookies.push(...setCookieHeader.split(/,(?=\s*\w+=)/));
    }

    return {
      user: mapBetterAuthUserToEEUser(result.user),
      token: result.token ?? undefined,
      cookies,
    };
  }

  /**

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Check the installed better-auth version and its signInEmail response shape; align with the version this provider expects.
  2. Log the raw response body to see what the server actually returned on success.
  3. Fall back to the sign-in flow's error path: retry signIn with corrected credentials in case the server returned 200 for a non-authenticated session.

Example fix

// before: expecting { user } but better-auth v1.x returns different shape
const result = await auth.signIn({ email, password });

// after: pin/upgrade better-auth to the version whose signInEmail returns { user, token }
// package.json: "better-auth": "<version compatible with @mastra/auth-better-auth>"
Defensive patterns

Strategy: type-guard

Validate before calling

const res = await providerSignInResponse;
if (res.ok) {
  const body = await res.json();
  if (!body?.user) console.warn('signIn returned 2xx without user; check better-auth version compatibility');
}

Type guard

function hasUser(r: unknown): r is { user: User; token?: string | null } {
  return typeof r === 'object' && r !== null && 'user' in r && typeof (r as { user: unknown }).user === 'object';
}

Try / catch

try {
  const result = await provider.signIn({ email, password });
  if (!hasUser(result)) throw new Error('Sign-in succeeded but no user was returned');
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid email or password') {
    console.error('Unexpected 2xx-without-user from better-auth; verify better-auth version');
  }
  throw e;
}

Prevention

When it happens

Trigger: signIn({ email, password }) where response.ok === true but `result?.user` is falsy — e.g. body shape changed across better-auth versions, empty body, or a 200 with only a token/error field instead of a user.

Common situations: better-auth version drift where signInEmail's success payload shape differs from the expected { user, token }; proxy/CDN returning an empty 200; interceptors swallowing the response body.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/f7454fe0ab9a371a. Report an issue: GitHub.