kwhitley / itty-router

A little router.
MIT License
1.7k stars 77 forks source link

Rate Limiting? #159

Closed osseonews closed 1 year ago

osseonews commented 1 year ago

There is a great example for setting up rate limiting at: https://upstash.com/blog/cloudflare-workers-rate-limiting

I am wondering if you can possibly provide an example implementing this with Itty-router? My problem is that its easy to create a function to run the limiter code for each route, but then you need to do this for every route. Is there a way to run this globally prior to actually calling any specific route? Maybe as middleware? (actually don't quite understand how middleware works in this package).

Thanks.

kwhitley commented 1 year ago

Are you trying to very specifically limit users/rates or more just trying to prevent DDoS and scripts-gone-wild from hammering your service?

If just the latter, I'd honestly skip all the redis/upstash bits and use a global/local cache (as they also do in the example).

  1. Add a global cache (e.g. a Map as they show). This will persist between requests (to the same edge worker), until the worker expires.
  2. Add a global middleware that does the following:
    1. Get a unique key from the request (e.g. something in the cf headers, an IP, whatever).
    2. Add or increment a value (starting at zero/1) in the map for that key.
    3. If the value at the key now exceeds a certain threshold, return an error Response.

This will use up minimal memory, be automatically cleaned up as the worker dies, etc - all without an external call. It's rough, but will prevent too many requests from a single user. You can of course go more granular with the middleware and add it to specific routes, or add something route-specific to the cache key (e.g. pathname) if you want to lock specific routes, or throttle them independently.

Hope this helps a bit!

osseonews commented 1 year ago

Thanks for the help. Yes, it is the latter I am looking to do. I can't figure out how to use Cloudflares rate limiting for workers. It's so confusing and surprising they dont just have a simple setting for this on every worker - i.e. prevent DDos with a toggle.

Anyway, I don't really what a global cache is, or how to use middleware yet in itty router. Hoping someone can write up a simple example of this.

kwhitley commented 1 year ago

Thanks for the help. Yes, it is the latter I am looking to do. I can't figure out how to use Cloudflares rate limiting for workers. It's so confusing and surprising they dont just have a simple setting for this on every worker - i.e. prevent DDos with a toggle.

Couldn't agree more. This should be a dashboard level setting IMO!

kwhitley commented 1 year ago

@osseonews

Middleware

Any route handler (that takes a request as the first argument) can be considered middleware, as there is no functional difference between any handler. By simply not returning, a handler will terminate and further handlers (and potentially routes, etc) will continue to execute. The first handler that returns anything will break the chain and return for the entire router.handle.

Read more about it here: https://itty.dev/itty-router/middleware

Global Cache

This is just a way to describe the fact that Workers technically can share the same code between requests, especially if from a similar edge/region. Because of this, you can (technically) use globally-scoped variables that persist between requests. This is super dirty, and the behavior may change at any point if they properly isolate the scope, but in the meantime, you can do it.

Because of this, you could, for instance, include a global object that kept track of urls hit and the number of times they've been hit. Then perhaps after a timeout of inactivity, clear the key to reset the counter.

import { Router } from 'itty-router'

export const router = Router()

// this is globally scoped, and actually persists for awhile between requests... try it and see!
let counter = 0

router
  // middleware
  .get('/reset', () => { counter = 0 })

  // middleware
  .get('/increment', () => { counter++ })

  // this route will catch all routes, displaying the current counter
  .get('*', () => `This counter's value is currently ${counter}.`)

Hope this ultra-simple example helps!

osseonews commented 1 year ago

Thank you! Very helpful.