API Rate Limited (429) Logs The User Out #446

Closed
opened 2026-03-13 07:46:22 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @daveycodez on GitHub (Dec 21, 2024).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Implement any rate limiting service into your API (I'm using Upstash in my middleware.ts)

If you trigger your rate limit then refresh the page, it will log the user out.

I think the easy fix is to not delete the session cookies and just ignore 429 responses from the server and then ideally retry them appropriately based on rate limiting headers.

x-ratelimit-limit:
60
x-ratelimit-remaining:
56
x-ratelimit-reset:
1734774840000

Retry the get-session request at x-ratelimit-reset and ignore the failed 429 response

Current vs. Expected behavior

Not force users to log back in after hitting a rate limit

What version of Better Auth are you using?

1.2.2

Provide environment information

Next.js Rate Limiting Middleware

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

Backend, Client

Auth config (if applicable)

No response

Additional context

No response

Originally created by @daveycodez on GitHub (Dec 21, 2024). ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce Implement any rate limiting service into your API (I'm using Upstash in my middleware.ts) If you trigger your rate limit then refresh the page, it will log the user out. I think the easy fix is to not delete the session cookies and just ignore 429 responses from the server and then ideally retry them appropriately based on rate limiting headers. x-ratelimit-limit: 60 x-ratelimit-remaining: 56 x-ratelimit-reset: 1734774840000 Retry the get-session request at x-ratelimit-reset and ignore the failed 429 response ### Current vs. Expected behavior Not force users to log back in after hitting a rate limit ### What version of Better Auth are you using? 1.2.2 ### Provide environment information ```bash Next.js Rate Limiting Middleware ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) _No response_ ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 07:46:22 -05:00
Author
Owner

@daveycodez commented on GitHub (Dec 21, 2024):

middleware.ts

import { NextFetchEvent, NextRequest, NextResponse } from "next/server"

import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
import { getSession } from "@/lib/auth"

const redis = new Redis({
    url: process.env.UPSTASH_REDIS_REST_URL!,
    token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

const ratelimit = new Ratelimit({
    redis: redis,
    limiter: Ratelimit.slidingWindow(60, "60 s"),
    analytics: true
})

export const allowedOrigins = ["http://localhost:3000"]

export const corsOptions = {
    "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization, neon-raw-text-output, neon-array-mode, neon-connection-string",
    // "Access-Control-Max-Age": "86400",
    "Access-Control-Allow-Credentials": "true",
}

export async function middleware(request: NextRequest, event: NextFetchEvent) {
    // Check the origin from the request
    const origin = request.headers.get("origin") ?? ""
    const isAllowedOrigin = allowedOrigins.includes(origin)

    // Handle preflighted requests
    const isPreflight = request.method === "OPTIONS"

    if (isPreflight) {
        const preflightHeaders = {
            ...(isAllowedOrigin && { "Access-Control-Allow-Origin": origin }),
            ...corsOptions,
        }
        return NextResponse.json({}, { headers: preflightHeaders })
    }

    // Handle simple requests
    const response = NextResponse.next()

    if (isAllowedOrigin) {
        response.headers.set("Access-Control-Allow-Origin", origin)
    }

    Object.entries(corsOptions).forEach(([key, value]) => {
        response.headers.set(key, value)
    })

    const headers = new Headers()
    if (request.headers.get("cookie")?.includes("auth-token")) {
        headers.set("cookie", request.headers.get("cookie")!)
    } else if (request.headers.has("authorization")) {
        headers.set("authorization", request.headers.get("authorization")!)
    }

    const session = await getSession({ headers })
    const identifier = session?.user.id || request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || "127.0.0.1"

    const { success, pending, limit, reset, remaining } =
        await ratelimit.limit(identifier)
    event.waitUntil(pending)

    response.headers.set("X-RateLimit-Limit", limit.toString())
    response.headers.set("X-RateLimit-Remaining", remaining.toString())
    response.headers.set("X-RateLimit-Reset", reset.toString())

    if (!success) {
        const errorResponse = NextResponse.json({ message: "Too Many Requests" }, { status: 429 })

        errorResponse.headers.set("X-RateLimit-Limit", limit.toString())
        errorResponse.headers.set("X-RateLimit-Remaining", remaining.toString())
        errorResponse.headers.set("X-RateLimit-Reset", reset.toString())

        if (isAllowedOrigin) {
            errorResponse.headers.set("Access-Control-Allow-Origin", origin)
        }

        Object.entries(corsOptions).forEach(([key, value]) => {
            errorResponse.headers.set(key, value)
        })

        return errorResponse
    }

    return response
}

export const config = {
    matcher: "/api/:path*",
}
@daveycodez commented on GitHub (Dec 21, 2024): middleware.ts ```ts import { NextFetchEvent, NextRequest, NextResponse } from "next/server" import { Ratelimit } from "@upstash/ratelimit" import { Redis } from "@upstash/redis" import { getSession } from "@/lib/auth" const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }) const ratelimit = new Ratelimit({ redis: redis, limiter: Ratelimit.slidingWindow(60, "60 s"), analytics: true }) export const allowedOrigins = ["http://localhost:3000"] export const corsOptions = { "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, neon-raw-text-output, neon-array-mode, neon-connection-string", // "Access-Control-Max-Age": "86400", "Access-Control-Allow-Credentials": "true", } export async function middleware(request: NextRequest, event: NextFetchEvent) { // Check the origin from the request const origin = request.headers.get("origin") ?? "" const isAllowedOrigin = allowedOrigins.includes(origin) // Handle preflighted requests const isPreflight = request.method === "OPTIONS" if (isPreflight) { const preflightHeaders = { ...(isAllowedOrigin && { "Access-Control-Allow-Origin": origin }), ...corsOptions, } return NextResponse.json({}, { headers: preflightHeaders }) } // Handle simple requests const response = NextResponse.next() if (isAllowedOrigin) { response.headers.set("Access-Control-Allow-Origin", origin) } Object.entries(corsOptions).forEach(([key, value]) => { response.headers.set(key, value) }) const headers = new Headers() if (request.headers.get("cookie")?.includes("auth-token")) { headers.set("cookie", request.headers.get("cookie")!) } else if (request.headers.has("authorization")) { headers.set("authorization", request.headers.get("authorization")!) } const session = await getSession({ headers }) const identifier = session?.user.id || request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || "127.0.0.1" const { success, pending, limit, reset, remaining } = await ratelimit.limit(identifier) event.waitUntil(pending) response.headers.set("X-RateLimit-Limit", limit.toString()) response.headers.set("X-RateLimit-Remaining", remaining.toString()) response.headers.set("X-RateLimit-Reset", reset.toString()) if (!success) { const errorResponse = NextResponse.json({ message: "Too Many Requests" }, { status: 429 }) errorResponse.headers.set("X-RateLimit-Limit", limit.toString()) errorResponse.headers.set("X-RateLimit-Remaining", remaining.toString()) errorResponse.headers.set("X-RateLimit-Reset", reset.toString()) if (isAllowedOrigin) { errorResponse.headers.set("Access-Control-Allow-Origin", origin) } Object.entries(corsOptions).forEach(([key, value]) => { errorResponse.headers.set(key, value) }) return errorResponse } return response } export const config = { matcher: "/api/:path*", } ```
Author
Owner

@daveycodez commented on GitHub (Dec 21, 2024):

Also is there a way to get the user id from the headers without calling getSession? Does getSession fire off an API request or just return the session from the cookie?

@daveycodez commented on GitHub (Dec 21, 2024): Also is there a way to get the user id from the headers without calling getSession? Does getSession fire off an API request or just return the session from the cookie?
Author
Owner

@Bekacru commented on GitHub (Dec 21, 2024):

I'm going to look into this further. This isn't intended behavior. The rate limiter runs before any of the endpoints are even reached, so it doesn't set cookies or invalidate anything.

@Bekacru commented on GitHub (Dec 21, 2024): I'm going to look into this further. This isn't intended behavior. The rate limiter runs before any of the endpoints are even reached, so it doesn't set cookies or invalidate anything.
Author
Owner

@Bekacru commented on GitHub (Dec 21, 2024):

Also is there a way to get the user id from the headers without calling getSession? Does getSession fire off an API request or just return the session from the cookie?

no, you'd need to call getSession, and it does trigger a request. But, if you have enabled cookieCache, it doesn't always hit the database.

@Bekacru commented on GitHub (Dec 21, 2024): > Also is there a way to get the user id from the headers without calling getSession? Does getSession fire off an API request or just return the session from the cookie? no, you'd need to call `getSession`, and it does trigger a request. But, if you have enabled `cookieCache`, it doesn't always hit the database.
Author
Owner

@daveycodez commented on GitHub (Dec 21, 2024):

Yea I'm seeing cookieCache now I think that will handle it

@daveycodez commented on GitHub (Dec 21, 2024): Yea I'm seeing cookieCache now I think that will handle it
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#446