Skip to content

Express 5 /*path Pattern Causes 404 on All Auth Routes #85

Description

@Brainisthekey

When using @thallesp/nestjs-better-auth@2.2.0 with Express 5 (via @nestjs/platform-express@11.x), all Better Auth routes return 404. This is caused by Express 5's wildcard pattern /*path behavior combined with how better-call constructs request URLs.

Environment

Package Version
@thallesp/nestjs-better-auth 2.2.0
better-auth 1.4.6
better-call 1.1.5
@nestjs/core 11.1.9
@nestjs/common 11.1.9
@nestjs/platform-express 11.1.9
express 5.1.0

The Problem

Root Cause

In nestjs-better-auth, the auth handler is mounted using:

// From dist/index.cjs line 322
this.adapter.httpAdapter.getInstance().use(`${basePath}/*path`, (req, res) => {
  // ...
  return handler(req, res);
});

With Express 5's /*path wildcard pattern, the request properties are set as:

  • req.url = / (just a slash)
  • req.baseUrl = /api/auth/sign-in/social (the FULL matched path)
  • req.originalUrl = /api/auth/sign-in/social

How better-call Handles This

In better-call@1.1.5 (file: node.js), the URL is constructed as:

function getRequest({ request, base, bodySizeLimit }) {
  const baseUrl = request?.baseUrl;
  const fullPath = baseUrl ? baseUrl + request.url : request.url;
  // ...
  return new Request(base + fullPath, { ... });
}

This results in: baseUrl + url = /api/auth/sign-in/social + / = /api/auth/sign-in/social/

The trailing slash causes Better Auth to not recognize the route, returning 404.

Expected Behavior (Express 4 / Without Wildcard)

With app.use('/api/auth', handler) (no wildcard):

  • req.url = /sign-in/social
  • req.baseUrl = /api/auth
  • Result: /api/auth + /sign-in/social = /api/auth/sign-in/social (correct!)

Proof of Concept

Adding debug logging confirms the issue:

this.adapter.httpAdapter.getInstance().use(`${basePath}/*path`, (req, res) => {
  console.log(`req.url=${req.url} | req.baseUrl=${req.baseUrl} | req.originalUrl=${req.originalUrl}`);
  // ...
});

Output:

req.url=/ | req.baseUrl=/api/auth/sign-in/social | req.originalUrl=/api/auth/sign-in/social

Current Workaround

Using the middleware option to fix the URL before the handler runs:

// auth.module.ts
import { Module } from "@nestjs/common";
import { AuthModule as BetterAuthModule } from "@thallesp/nestjs-better-auth";
import { auth } from "./auth";

@Module({
  imports: [
    BetterAuthModule.forRoot({
      auth,
      // Fix for Express 5: The /*path pattern sets req.url=/ and req.baseUrl=full_path
      // better-call concatenates baseUrl+url creating a trailing slash that causes 404
      // This middleware restores req.url to the full path before the handler runs
      middleware: (req, _res, next) => {
        req.url = req.originalUrl;
        req.baseUrl = "";
        next();
      },
    }),
  ],
  exports: [BetterAuthModule],
})
export class AuthModule {}

Better Auth Configuration (for reference)

// auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@app/database/generated/prisma";
import { env } from "../config/env.config";

const prisma = new PrismaClient();

export const auth = betterAuth({
  baseURL: env.auth.betterAuthUrl,
  database: prismaAdapter(prisma, {
    provider: "postgresql",
    transaction: true,
  }),
  socialProviders: {
    google: {
      clientId: env.auth.googleClientId,
      clientSecret: env.auth.googleClientSecret,
      scope: ["email", "profile"],
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // Cache for 5 minutes
    },
  },
  trustedOrigins: [env.auth.betterAuthUrl, env.web.url],
});

Related Issues

Notes

  • The better-call fix (PR Support for Database Hooks? #32) assumes req.baseUrl is the mount point and req.url is the remaining path
  • Express 5's /*path pattern violates this assumption by putting the FULL path in req.baseUrl
  • The official Better Auth Express docs suggest using /*splat for Express 5, not /*path

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions