The rate limit feature in NextJS does not function properly when used with middleware

Currently, I have implemented a rate limit using the lru-cache based on the official example provided by Vercel, and it is functioning flawlessly.

My intention was to consolidate the try-catch block in a Middleware to handle all routes, instead of duplicating it for each route. However, I encountered an issue where the CACHE_TOKEN value only increments within the api routes and remains stagnant in the Middleware.

Vercel's Example: https://github.com/vercel/next.js/tree/canary/examples/api-routes-rate-limit

SRC/ HELPERS/ RATELIMIT.JS

import LRU from 'lru-cache'

export default function rateLimit() {
  const tokenCache = new LRU({
    max: 500, // Max 500 users per second
    ttl: 1 * 60000, // 1 minute in milliseconds
  })

  const token = 'CACHE_TOKEN'
  const limit = 3

  return {
    check: (res) =>
      new Promise((resolve, reject) => {
        const tokenCount = tokenCache.get(token) || [0]
        if (tokenCount[0] === 0) {
          tokenCache.set(token, tokenCount)
        }

        tokenCount[0] += 1
        const currentUsage = tokenCount[0]
        const isRateLimited = currentUsage >= limit

        res.headers.set('X-RateLimit-Limit', limit)
        res.headers.set('X-RateLimit-Remaining', isRateLimited ? 0 : limit - currentUsage)

        console.log(tokenCache.get(token))
        /* 
        using api route: [ 1 ] [ 2 ] [ 3 ] 
        using middleware: [ 1 ] [ 1 ] [ 1 ] [ 1 ] [ 1 ] ...
        */

        return isRateLimited ? reject() : resolve()
      }),
  }
}

SRC/ PAGES/ API/ HELLO.JS

import { NextResponse } from 'next/server'
import rateLimit from '@/helpers/rateLimit'

const limiter = rateLimit()

export default async function handler(req, res) {
  const response = NextResponse.next()

  try {
    await limiter.check(response) // 10 requests per minute
  } catch (error) {
    console.log(error)
    return res.status(429).json({ error: 'Rate limit exceeded' })
  }

  return res.status(200).json({ message: 'Hello World' })
}

SRC/ MIDDLEWARE.JS

import { NextResponse } from 'next/server'
import rateLimit from '@/helpers/rateLimit'

const limiter = rateLimit()

export async function middleware(req) {
  const response = NextResponse.next()

  try {
    await limiter.check(response) // 10 requests per minute
  } catch (error) {
    console.log(error)
    return NextResponse.json({ error: 'Rate limit exceeded' })
  }
}

Answer №1

After some modifications, I successfully integrated this with middleware functionality.

Here is my custom ratelimiter implementation:

import { LRUCache } from 'lru-cache';

type Options = {
  uniqueTokenPerInterval?: number; // maximum unique tokens allowed in the time frame
  interval?: number; // time interval in milliseconds
  limit: number; // maximum number of requests within the interval
};

export default function rateLimit(options?: Options) {
  const tokenCache = new LRUCache({
    max: options?.uniqueTokenPerInterval || 50,
    ttl: options?.interval || 60 * 1000,
  });

  return {
    check: (token: string, limit = options?.limit || 100) => {
      const tokenCount = (tokenCache.get(token) as number[]) || [0];
      if (tokenCount[0] === 0) {
        tokenCache.set(token, tokenCount);
      }
      tokenCount[0] += 1;

      const currentUsage = tokenCount[0];
      const isRateLimited = currentUsage >= limit;

      return {
        isRateLimited,
        currentUsage,
        limit,
      };
    },
  };
}

Here is how I utilize it within the middleware:

import rateLimit from 'lib/ratelimit';
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import { NextRequestWithAuth, withAuth } from 'next-auth/middleware';

const limiter = rateLimit({
  limit: 1000,
});

const middleware = async (req: NextRequestWithAuth) => {
  const token = await getToken({
    req,
  });

  const { access_token } = token ?? {};

  if (!access_token) {
    return new Response('Unauthorized', {
      status: 401,
      statusText: 'Unauthorized',
    });
  }

   const { isRateLimited, currentUsage, limit } = limiter.check(access_token);
  // console.log(`Rate limit: ${currentUsage}/${limit}`);

  if (isRateLimited) {
    return new Response('Rate limit reached', {
      status: 429,
      statusText: 'Too Many Requests',
    });
  }

 

  return NextResponse.next();
};

export default withAuth(middleware);

export const config = { matcher: ['/api/v1/:path*'] };

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to display a component with multiple pieces of content?

I have a tool that generates card components, extracting data from a const array and displaying it in a table format within a card UI component. I am looking to add an ADD button inside each card to open a modal with input fields. However, the issue is tha ...

Yep, implementing conditional logic with the `when` keyword and radio buttons

I seem to be encountering an issue with my implementation (probably something trivial). I am utilizing React Hook Form along with Yup and attempting to establish a condition based on the selection of a radio group. The scenario is as follows: if the first ...

Failed PHP response when jQuery was called

I'm working on a project that involves two files: one PHP and one HTML. The HTML file acts as the user interface where users input their queries, while the PHP file handles the processing and events before returning the output back to the HTML file. I ...

Packing third-party npm modules with Webpack for seamless integration

Description I am currently working on a project that involves nodejs, TypeScript, and express. The source files with a *.ts extension are being bundled using webpack, while the node_modules folder is excluded using webpack-node-externals. However, when I ...

Showing an array in angular.js

When sending data to a PHP file, the file processes it and performs an SQL search, returning a list of names in a for each statement. For example: Ryan, Jack, Billy, Sarah. However, when echoing the response in Angular, all the names are joined together l ...

Is there a way to assign a null value to an empty material UI text field when submitting the form, rather than an empty string?

One issue I am facing is that the default value of the text field is zero, but when I submit the form, the value of the text field becomes an empty string instead. This is not the intended behavior as I want the value to remain zero in the end. How can I r ...

Express callback delaying with setTimeout

I'm working on a route that involves setting a data attribute called "active" to true initially, but then switching it to false after an hour. I'm curious if it's considered bad practice or feasible to use a setTimeout function within the ex ...

Submitting jQuery Ajax forms multiple times using the POST method

After trying various solutions for this issue, none seem to effectively address my problem. Here are some examples: $("form#sending-notice-form").unbind('submit').bind('submit', function (e) { e.preventDefault(); .. }); While ...

Building secure and responsive routes using next.js middleware

After setting up my routes.ts file to store protected routes, I encountered an issue with dynamic URLs not being properly secured. Even though regular routes like '/profile' were restricted for unauthenticated users, the dynamic routes remained a ...

Uploading files using Remix.run: Transforming a File object into a string during the action

I'm currently developing a Remix application that allows users to upload files through a form. I have a handler function for handling the form submission, which takes all the form data, including file attachments, and sends it to my action. The probl ...

The jQuery validation feature permits entering a code that falls within the range of user1 to user100

Here is an example where the user can only enter a code between 1 and 100. Otherwise, it will return false. var regexCode = /var regexEmail = /^0*(?:[1-9][0-9]?|100)$/; $(document).on('change','#code',function() ...

Angular5 causing all divs to have click events at once instead of individually triggered

I am a beginner when it comes to working with Angular and I have encountered an issue. I created a click event on a FAQ page in Angular 5, but the problem is that when I click on one FAQ, they all open up instead of just the targeted one. Here is my TypeS ...

What might be causing the issue of a click handler not registering during the initial page load when using Enquire.js

I am experimenting with different effects on various breakpoints. My main goal is to achieve the following behavior: when a category is clicked from the list within 720px, the category list should fade out while the data is revealed in its place. However, ...

Is Nuxt's FingerprintJS Module the Ultimate Server and Client Solution?

I am currently using fingerprintJS in my NuxtJS+Firebase project VuexStore. When I call the function on the client side, I can retrieve the Visitor ID. However, I am encountering issues when trying to use it on the server side, such as in nuxtServerInit. ...

Passing the socket.io instance to an express route

I am currently working on developing a nodejs application that will utilize various web APIs to search for information. The goal is to send the results to the client in real-time using socket.io, with jQuery handling the front end display of the data. Wha ...

What is the best way to retrieve and display a PDF file through an API in VueJS?

I am looking to display a file from my API in my VueJS client. Specifically, when accessing a certain URL, the file (pdf, text, or image) should open if the browser supports it (similar to how Chrome opens PDFs). I want to achieve this using VueJS or just ...

Upgrade to the most recent versions of packages using npm

When using npm install --save <package_name>, it typically installs the latest stable version of the package. If you want to specifically install the most recent release, such as Bootstrap v4, you would need to use npm install <a href="/cdn-cgi/l ...

Leveraging a JavaScript variable declared outside the function to successfully transfer data to my AJAX function

Every time the enter key is pressed, my AJAX function gets executed. I used to pass a set of javascript variables equal to elements in the HTML (the contents of a text area) as data for the AJAX function. Now, I want these JS variables to hold values from ...

Utilizing child component HTTP responses within a parent component in Angular: a comprehensive guide

As a newcomer to Angular, I find myself struggling with http requests in my application. The issue arises when I have component A responsible for retrieving a list of IDs that need to be accessed by multiple other components. In component B, I attempted t ...

When the back button is clicked, pagination returns to number 1

I'm currently working on implementing Pagination in ReactJS and facing an issue where the pagination resets to page 1 when I navigate away from the current page and then come back. Ideally, I would like to resume from the same page where I left off. H ...