-
-
Notifications
You must be signed in to change notification settings - Fork 159
feat(server): ratelimit helpers & plugins #1183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
a67cefb
init
dinwwwh c518f42
upstash ratelimit adapter
dinwwwh 2f4f082
improve
dinwwwh 2d8b9c6
improve
dinwwwh b247834
improve
dinwwwh c502667
ratelimit: add ioredis adapter + tests; unify result shape; adapt ups…
dinwwwh 63e5751
improve
dinwwwh 728d884
memory adapter
dinwwwh abdb1a6
improve
dinwwwh f407247
improve
dinwwwh d500f2b
improve
dinwwwh 382abb3
wip
dinwwwh 069ff2b
handler plugin tests
dinwwwh 211d193
tests middleware
dinwwwh 6ed4457
ete test
dinwwwh 7a2c521
docs
dinwwwh 1d7953e
improve
dinwwwh cdd1bd2
fix
dinwwwh 5b91545
fix
dinwwwh 200e651
fix
dinwwwh 73eeb5e
improve
dinwwwh a296ec0
improve docs
dinwwwh 7a99cb7
fix: clamp retry-after header to 0 when reset time is in the past
dinwwwh 131295c
Merge branch 'main' into unnoq/issue910
dinwwwh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| --- | ||
| title: Rate Limit | ||
| description: Rate limiting features for oRPC with multiple adapters support. | ||
| --- | ||
|
|
||
| # Rate Limit | ||
|
|
||
| The Rate Limit package provides flexible rate limiting for oRPC with multiple storage backend support. It includes adapters for in-memory, Redis, and Upstash, along with middleware and plugin helpers for seamless integration. | ||
|
|
||
| ## Installation | ||
|
|
||
| ::: code-group | ||
|
|
||
| ```sh [npm] | ||
| npm install @orpc/experimental-ratelimit@latest | ||
| ``` | ||
|
|
||
| ```sh [yarn] | ||
| yarn add @orpc/experimental-ratelimit@latest | ||
| ``` | ||
|
|
||
| ```sh [pnpm] | ||
| pnpm add @orpc/experimental-ratelimit@latest | ||
| ``` | ||
|
|
||
| ```sh [bun] | ||
| bun add @orpc/experimental-ratelimit@latest | ||
| ``` | ||
|
|
||
| ```sh [deno] | ||
| deno add npm:@orpc/experimental-ratelimit@latest | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Available Adapters | ||
|
|
||
| ### Memory Adapter | ||
|
|
||
| A simple in-memory rate limiter using a sliding window log algorithm. Ideal for single-instance applications or development. | ||
|
|
||
| ```ts | ||
| import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory' | ||
|
|
||
| const limiter = new MemoryRatelimiter({ | ||
| maxRequests: 10, // Maximum requests allowed | ||
| window: 60000, // Time window in milliseconds (60 seconds) | ||
| }) | ||
| ``` | ||
|
|
||
| ### Redis Adapter | ||
|
|
||
| Redis-based rate limiter using atomic Lua scripts for distributed rate limiting. | ||
|
|
||
| ```ts | ||
| import { RedisRatelimiter } from '@orpc/experimental-ratelimit/redis' | ||
| import { Redis } from 'ioredis' | ||
|
|
||
| const redis = new Redis('redis://localhost:6379') | ||
|
|
||
| const limiter = new RedisRatelimiter({ | ||
| eval: async (script, numKeys, ...rest) => { | ||
| return redis.eval(script, numKeys, ...rest) | ||
| }, | ||
| maxRequests: 100, | ||
| window: 60000, | ||
| prefix: 'orpc:ratelimit:', // Optional key prefix | ||
| }) | ||
| ``` | ||
|
|
||
| ::: info | ||
| You can use any Redis client that supports Lua script evaluation by providing an `eval` function. | ||
| ::: | ||
|
|
||
| ### Upstash Adapter | ||
|
|
||
| Adapter for [@upstash/ratelimit](https://www.npmjs.com/package/@upstash/ratelimit), optimized for serverless environments like Vercel Edge and Cloudflare Workers. | ||
|
|
||
| ```ts | ||
| import { Ratelimit } from '@upstash/ratelimit' | ||
| import { Redis } from '@upstash/redis' | ||
| import { UpstashRatelimiter } from '@orpc/experimental-ratelimit/upstash-ratelimit' | ||
|
|
||
| const redis = Redis.fromEnv() | ||
|
|
||
| const ratelimit = new Ratelimit({ | ||
| redis, | ||
| limiter: Ratelimit.slidingWindow(10, '60 s'), | ||
| prefix: 'my-app:', | ||
| }) | ||
|
|
||
| const limiter = new UpstashRatelimiter(ratelimit) | ||
| ``` | ||
|
|
||
| ::: tip Edge Runtime Support | ||
| For Edge runtime like Vercel Edge or Cloudflare Workers, pass the `waitUntil` function to better handle background tasks: | ||
|
|
||
| ```ts | ||
| const limiter = new UpstashRatelimiter(ratelimit, { | ||
| waitUntil: ctx.waitUntil.bind(ctx), | ||
| }) | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Blocking Mode | ||
|
|
||
| Some adapters support blocking mode, which waits for the rate limit to reset instead of immediately rejecting requests. | ||
|
|
||
| ```ts | ||
| const limiter = new MemoryRatelimiter({ | ||
| maxRequests: 10, | ||
| window: 60000, | ||
| blockingUntilReady: { | ||
| enabled: true, | ||
| timeout: 5000, // Wait up to 5 seconds | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| ## Manual Usage | ||
|
|
||
| You can use adapters directly without middleware for custom rate limiting logic: | ||
|
|
||
| ```ts twoslash | ||
| import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory' | ||
| import { ORPCError } from '@orpc/server' | ||
|
|
||
| const limiter = new MemoryRatelimiter({ | ||
| maxRequests: 5, | ||
| window: 60000, | ||
| }) | ||
|
|
||
| const result = await limiter.limit('user:123') | ||
|
|
||
| if (!result.success) { | ||
| throw new ORPCError('TOO_MANY_REQUESTS', { | ||
| data: { | ||
| limit: result.limit, | ||
| remaining: result.remaining, | ||
| reset: result.reset, | ||
| }, | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| ## `createRatelimitMiddleware` | ||
|
|
||
| The `createRatelimitMiddleware` helper creates middleware for oRPC procedures to enforce rate limits. | ||
|
|
||
| ```ts twoslash | ||
| import { call, os } from '@orpc/server' | ||
| import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory' | ||
| import { createRatelimitMiddleware, Ratelimiter } from '@orpc/experimental-ratelimit' | ||
| import { z } from 'zod' | ||
|
|
||
| const loginProcedure = os | ||
| .$context<{ ratelimiter: Ratelimiter }>() | ||
| .input(z.object({ email: z.email() })) | ||
| .use( | ||
| createRatelimitMiddleware({ | ||
| limiter: ({ context }) => context.ratelimiter, | ||
| key: ({ context }, input) => `login:${input.email}`, | ||
| }), | ||
| ) | ||
| .handler(({ input }) => { | ||
| return { success: true } | ||
| }) | ||
|
|
||
| const ratelimiter = new MemoryRatelimiter({ | ||
| maxRequests: 10, | ||
| window: 60000, | ||
| }) | ||
|
|
||
| const result = await call( | ||
| loginProcedure, | ||
| { email: 'user@example.com' }, | ||
| { context: { ratelimiter } } | ||
| ) | ||
| ``` | ||
|
|
||
| ::: info Automatic Deduplication | ||
| The `createRatelimitMiddleware` automatically deduplicates rate limit checks when the same `limiter` and `key` combination is used multiple times in a request chain. This behavior follows the [Dedupe Middleware Best Practice](/docs/best-practices/dedupe-middleware). To disable deduplication, set the `dedupe: false` option. | ||
| ::: | ||
|
|
||
| ::: tip Conditional Limiter | ||
| You can dynamically choose different limiters based on context: | ||
|
|
||
| ```ts | ||
| const premiumLimiter = new MemoryRatelimiter({ | ||
| maxRequests: 100, | ||
| window: 60000, | ||
| }) | ||
|
|
||
| const standardLimiter = new MemoryRatelimiter({ | ||
| maxRequests: 10, | ||
| window: 60000, | ||
| }) | ||
|
|
||
| const result = await call( | ||
| loginProcedure, | ||
| { email: 'user@example.com' }, | ||
| { | ||
| context: { | ||
| ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter, | ||
|
dinwwwh marked this conversation as resolved.
|
||
| }, | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Handler Plugin | ||
|
|
||
| The `RatelimitHandlerPlugin` automatically adds HTTP rate-limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with middleware created by [`createRatelimitMiddleware`](#createratelimitmiddleware). | ||
|
|
||
| ```ts | ||
| import { RatelimitHandlerPlugin } from '@orpc/experimental-ratelimit' | ||
|
|
||
| const handler = new RPCHandler(router, { | ||
| plugins: [ | ||
| new RatelimitHandlerPlugin(), | ||
| ], | ||
| }) | ||
| ``` | ||
|
|
||
| ::: info | ||
| The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or other custom handlers. | ||
| ::: | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Hidden folders and files | ||
| .* | ||
| !.gitignore | ||
| !.*.example | ||
|
|
||
| # Common generated folders | ||
| logs/ | ||
| node_modules/ | ||
| out/ | ||
| dist/ | ||
| dist-ssr/ | ||
| build/ | ||
| coverage/ | ||
| temp/ | ||
|
|
||
| # Common generated files | ||
| *.log | ||
| *.log.* | ||
| *.tsbuildinfo | ||
| *.vitest-temp.json | ||
| vite.config.ts.timestamp-* | ||
| vitest.config.ts.timestamp-* | ||
|
|
||
| # Common manual ignore files | ||
| *.local | ||
| *.pem |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <div align="center"> | ||
| <image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" /> | ||
| </div> | ||
|
|
||
| <h1></h1> | ||
|
|
||
| <div align="center"> | ||
| <a href="https://codecov.io/gh/unnoq/orpc"> | ||
| <img alt="codecov" src="https://codecov.io/gh/unnoq/orpc/branch/main/graph/badge.svg"> | ||
| </a> | ||
| <a href="https://www.npmjs.com/package/@orpc/experimental-ratelimit"> | ||
| <img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fexperimental-ratelimit?logo=npm" /> | ||
| </a> | ||
| <a href="https://github.com/unnoq/orpc/blob/main/LICENSE"> | ||
| <img alt="MIT License" src="https://img.shields.io/github/license/unnoq/orpc?logo=open-source-initiative" /> | ||
| </a> | ||
| <a href="https://discord.gg/TXEbwRBvQn"> | ||
| <img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" /> | ||
| </a> | ||
| <a href="https://deepwiki.com/unnoq/orpc"> | ||
| <img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"> | ||
| </a> | ||
| </div> | ||
|
|
||
| <h3 align="center">Typesafe APIs Made Simple 🪄</h3> | ||
|
|
||
| **oRPC is a powerful combination of RPC and OpenAPI**, makes it easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards | ||
|
|
||
| --- | ||
|
|
||
| ## Highlights | ||
|
|
||
| - **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server. | ||
| - **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard. | ||
| - **📝 Contract-First Development**: Optionally define your API contract before implementation. | ||
| - **🔍 First-Class OpenTelemetry**: Seamlessly integrate with OpenTelemetry for observability. | ||
| - **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), SWR, Pinia Colada, and more. | ||
| - **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms. | ||
| - **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators. | ||
| - **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more. | ||
| - **⏱️ Lazy Router**: Enhance cold start times with our lazy routing feature. | ||
| - **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming. | ||
| - **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond. | ||
| - **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors. | ||
|
|
||
| ## Documentation | ||
|
|
||
| You can find the full documentation [here](https://orpc.unnoq.com). | ||
|
|
||
| ## Packages | ||
|
|
||
| - [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract. | ||
| - [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract. | ||
| - [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety. | ||
| - [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests. | ||
| - [@orpc/otel](https://www.npmjs.com/package/@orpc/otel): [OpenTelemetry](https://opentelemetry.io/) integration for observability. | ||
| - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/). | ||
| - [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions. | ||
| - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration. | ||
| - [@orpc/experimental-react-swr](https://www.npmjs.com/package/@orpc/experimental-react-swr): [SWR](https://swr.vercel.app/) integration. | ||
| - [@orpc/vue-colada](https://www.npmjs.com/package/@orpc/vue-colada): Integration with [Pinia Colada](https://pinia-colada.esm.dev/). | ||
| - [@orpc/hey-api](https://www.npmjs.com/package/@orpc/hey-api): [Hey API](https://heyapi.dev/) integration. | ||
| - [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): More schemas that [Zod](https://zod.dev/) doesn't support yet. | ||
| - [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): OpenAPI spec generation from [Valibot](https://valibot.dev/). | ||
| - [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): OpenAPI spec generation from [ArkType](https://arktype.io/). | ||
|
|
||
| ## `@orpc/experimental-ratelimit` | ||
|
|
||
| Rate Limiting Feature for oRPC | ||
|
|
||
| ## Sponsors | ||
|
|
||
| <p align="center"> | ||
| <a href="https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg"> | ||
| <img src='https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg'/> | ||
| </a> | ||
| </p> | ||
|
|
||
| ## License | ||
|
|
||
| Distributed under the MIT License. See [LICENSE](https://github.com/unnoq/orpc/blob/main/LICENSE) for more information. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.