Skip to content

Commit

Permalink
rewrite errorResponse, and add a way to inject server into plugin (#40)
Browse files Browse the repository at this point in the history
* - add update all dependencies ( using pnpm )
- update dependencies

* - remove cache bun.lockb

* - add using server
- add getServer type

* better getServer ?

* change get server to using object store server

* - add throw error when rate-limit reached
- add type needRequestIP
- edit getserver for support only need requestIP function

* add getServer Docs

* add throwOnError Docs

* export default option

* fix for #40 (review)

* update dependencies

* add default options use as global option in example

* fix: export variable name as-is

* refactor: grouping if condition

* refactor: errorResponse

* chore: regenerate lockfile

* refactor: getServer -> injectServer

* docs: changeset

* fix: redundant typeof check

---------

Co-authored-by: Phumrapee Limpianchop <[email protected]>
  • Loading branch information
armada45-pixel and rayriffy authored Apr 26, 2024
1 parent 762a4af commit 17f10e3
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 50 deletions.
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

0 comments on commit 17f10e3

Please sign in to comment.