Skip to content
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

rewrite errorResponse, and add a way to inject server into plugin #40

Merged
merged 19 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-needles-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"elysia-rate-limit": major
---

**BREAKING CHANGES** remove `responseCode`, and `responseMessage` in favor of new `errorResponse` option. please consult with documentation for more details
5 changes: 5 additions & 0 deletions .changeset/large-walls-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"elysia-rate-limit": minor
---

added `injectServer` option
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,8 @@ dist
.svelte-kit

# End of https://www.toptal.com/developers/gitignore/api/node,macos

#lock file
package-lock.json
pnpm-lock.yaml
yaml.lock
107 changes: 86 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Lightweight rate limiter plugin for [Elysia.js](https://elysiajs.com/)
![NPM Downloads](https://img.shields.io/npm/dw/elysia-rate-limit)
![NPM License](https://img.shields.io/npm/l/elysia-rate-limit)


## Install

```
Expand Down Expand Up @@ -57,28 +56,78 @@ Default: `10`

Maximum of request to be allowed during 1 `duration` timeframe.

### responseCode
### errorResponse

`number`
`string | Response | Error`

Default: `429`
Default: `rate-limit reached`

HTTP response code to be sent when the rate limit was reached.
By default,
it will return `429 Too Many Requests`
referring to [RFC 6585 specification](https://www.rfc-editor.org/rfc/rfc6585#section-4)
Response to be sent when the rate limit is reached.

### responseMessage
If you define a value as a string,
then it will be sent as a plain text response with status code 429. If you define a value as a `Response` object,
then it will be sent as is.
And if you define a value as an `Error` object, then it will be thrown as an error.

`any`
<details>
<summary>Example for <code>Response</code> object response</summary>

Default: `rate-limit reached`
```ts
new Elysia()
.use(
rateLimit({
errorResponse: new Response("rate-limited", {
status: 429,
headers: new Headers({
'Content-Type': 'text/plain',
'Custom-Header': 'custom',
}),
}),
})
)
```
</details>

Message to be sent when the rate limit was reached
<details>
<summary>Example for <code>Error</code> object response</summary>

```ts
import { HttpStatusEnum } from 'elysia-http-status-code/status'

export class RateLimitError extends Error {
constructor(
public message: string = 'rate-limited',
public detail: string = '',
public status: number = HttpStatusEnum.HTTP_429_TOO_MANY_REQUESTS // or just 429
) {
super(message)
}
}

new Elysia()
.use(
rateLimit({
errorResponse: new RateLimitError(),
})
)
// use with error hanlder
.error({
rateLimited: RateLimitError,
})
.onError({ as: 'global' }, ({ code }) => {
switch (code) {
case 'rateLimited':
return code
break
}
})
```

</details>

### scoping

`'global' | 'local'`
`'global' | 'local' | 'scoped'`

Default: `'global'`

Expand All @@ -89,6 +138,8 @@ to choose scope `local` apply to only current instance and descendant only.
But by default,
rate limit plugin will apply to all instances that apply the plugin.

Read more : [Scope - ElysiaJS | ElysiaJS](https://elysiajs.com/essential/scope.html#scope)

### generator

`<T extends object>(equest: Request, server: Server | null, derived: T) => MaybePromise<string>`
Expand Down Expand Up @@ -119,7 +170,7 @@ import type { Generator } from 'elysia-rate-limit'

const ipGenerator: Generator<{ ip: SocketAddress }> = (_req, _serv, { ip }) => {
return ip
}
}
```

### countFailedRequest
Expand Down Expand Up @@ -153,13 +204,12 @@ By default, context implementation, caching will be an LRU cache with a maximum
```ts
import { DefaultContext } from 'elysia-rate-limit'

new Elysia()
.use(
rateLimit({
// define max cache size to 10,000
context: new DefaultContext(10_000),
})
)
new Elysia().use(
rateLimit({
// define max cache size to 10,000
context: new DefaultContext(10_000),
})
)
```

### skip
Expand All @@ -173,3 +223,18 @@ to determine that should this request be counted into rate-limit
or not based on information given by `Request` object
(i.e., Skip counting rate-limit on some route) and the key of the given request,
by default, this will always return `false` which means counted everything.

### injectServer

`() => Server`

Default: `undefined`

A function to inject server instance to the plugin,
this is useful
when you want to use default key generator in detached Elysia instances.
You can check out the example [here](./example/muliInstanceInjected.ts).

Please use this function as a last resort,
as defining this option will make plugin to make an extra function call,
which may affect performance.
Binary file modified bun.lockb
Binary file not shown.
38 changes: 38 additions & 0 deletions example/muliInstanceInjected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'

import { rateLimit } from '../src'

import type { Options } from '../src'
import type { Server } from 'bun'

let server: Server | null

const options: Partial<Options> = {
scoping: 'local',
duration: 200 * 1000,
injectServer: () => {
return server!
},
}

// const keyGenerator: Generator<{ ip: string }> = async (req, server, { ip }) => Bun.hash(JSON.stringify(ip)).toString()

const aInstance = new Elysia()
.use(rateLimit(options))
.get('/a', () => 'a')

const bInstance = new Elysia()
.use(rateLimit(options))
.get('/b', () => 'b')

const app = new Elysia()
.use(swagger())
.use(aInstance)
.use(bInstance)
.get('/', () => 'hello')
.listen(3000, () => {
console.log('🦊 Swagger is active at: http://localhost:3000/swagger')
})

server = app.server
1 change: 1 addition & 0 deletions example/multiInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { swagger } from '@elysiajs/swagger'
import { ip } from 'elysia-ip' // just a glitch pls ignore this

import { rateLimit } from '../src'

import type { Generator } from '../src'

const keyGenerator: Generator<{ ip: string }> = async (req, server, { ip }) => Bun.hash(JSON.stringify(ip)).toString()
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@
"license": "MIT",
"dependencies": {
"debug": "4.3.4",
"lru-cache": "10.2.0"
"lru-cache": "10.2.1"
},
"devDependencies": {
"@changesets/cli": "2.26.2",
"@elysiajs/swagger": "^1.0.3",
"@changesets/cli": "2.27.1",
"@elysiajs/swagger": "^1.0.4",
"@types/debug": "^4.1.12",
"bun-types": "^1.0.14",
"elysia": "1.0.0",
"bun-types": "^1.1.4",
"elysia": "1.0.14",
"elysia-ip": "^1.0.3",
"prettier": "^2.8.4",
"typescript": "^5.3.2"
"prettier": "^3.2.5",
"typescript": "^5.4.5"
},
"peerDependencies": {
"elysia": ">= 1.0.0"
Expand Down
6 changes: 5 additions & 1 deletion src/@types/Generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Server } from 'bun'
import type { MaybePromise } from 'elysia'

export type Generator<T extends object = {}> = (equest: Request, server: Server | null, derived: T) => MaybePromise<string>
export type Generator<T extends object = {}> = (
request: Request,
server: Server | null,
derived: T
) => MaybePromise<string>
22 changes: 14 additions & 8 deletions src/@types/Options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Server } from 'bun'

import type { Context } from './Context'
import { Generator } from './Generator'
import type { Generator } from './Generator'

export interface Options {
// The duration for plugin to remember the requests (Default: 60000ms)
Expand All @@ -8,14 +10,12 @@ export interface Options {
// Maximum of requests per specified duration (Default: 10)
max: number

// status code to be sent when rate-limit reached (Default: 429 per RFC 6585 specification)
responseCode: number

// message response when rate-limit reached (Default: rate-limit reached)
responseMessage: any
// Object to response when rate-limit reached
errorResponse: string | Response | Error

// scoping for rate limiting, set global by default to affect every request, but you can adjust to local to affect only within current instance
scoping: 'global' | 'local'
// scoping for rate limiting, set global by default to affect every request,
// but you can adjust to local to affect only within current instance
scoping: 'global' | 'local' | 'scoped'

// should the rate limit be counted when a request result is failed (Default: false)
countFailedRequest: boolean
Expand All @@ -30,4 +30,10 @@ export interface Options {
// not counting rate limit for some requests
// (Default: always return false)
skip: (req: Request, key?: string) => boolean | Promise<boolean>

// an explicit way to inject server instance to generator function
// uses this as last resort only
// since this function will slightly reduce server performance
// (Default: not defined)
injectServer?: () => Server
}
3 changes: 1 addition & 2 deletions src/constants/defaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import type { Options } from '../@types/Options'
export const defaultOptions: Omit<Options, 'context'> = {
duration: 60000,
max: 10,
responseCode: 429,
responseMessage: 'rate-limit reached',
errorResponse: 'rate-limit reached',
scoping: 'global',
countFailedRequest: false,
generator: defaultKeyGenerator,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { plugin as rateLimit } from './services/plugin'
export { DefaultContext } from './services/defaultContext'
export { defaultOptions } from './constants/defaultOptions'

export type { Context } from './@types/Context'
export type { Options } from './@types/Options'
Expand Down
Loading
Loading