Skip to content

Commit a3c8317

Browse files
committed
feat(deserve): request timeout, session HMAC signing, and docs alignment 🔐
- Add requestTimeoutMs to Handler and Router, return 503 on timeout - Body limit: remove redundant return so body stream wrapping runs when no early reject - Docs (EN+ID): body-limit stream behavior, global middleware signature and hang note - Docs (EN+ID): request timeout in routes-config and server-config - Docs (EN+ID): session cookieSecret required, setSession async, HMAC - Session: require cookieSecret, sign cookie with HMAC-SHA256, async setSession - Types: HandlerOptions and RouterOptions requestTimeoutMs, SessionOptions cookieSecret required - Update session tests for cookieSecret and async setSession
1 parent f492ef5 commit a3c8317

19 files changed

Lines changed: 512 additions & 150 deletions

docs/en/getting-started/routes-configuration.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ Configure Deserve routes directory to match your project structure.
44

55
## Router Options
66

7-
The `Router` constructor accepts configuration options. The main one is `routesDir` (directory for your route files):
7+
The `Router` constructor accepts configuration options. Main options: `routesDir` (directory for route files), `requestTimeoutMs` (request timeout), and optional `errorResponseBuilder` / `staticHandler`.
88

99
```typescript
1010
// 1. Import Router
1111
import { Router } from '@neabyte/deserve'
1212

13-
// 2. Set custom routesDir (default: ./routes)
13+
// 2. Set custom routesDir and optional request timeout (default: ./routes, no timeout)
1414
const router = new Router({
15-
routesDir: 'src/routes'
15+
routesDir: 'src/routes',
16+
requestTimeoutMs: 30_000
1617
})
1718
```
1819

@@ -32,6 +33,17 @@ const router = new Router({
3233
})
3334
```
3435

36+
### `requestTimeoutMs`
37+
38+
Optional timeout in milliseconds for the full request (middleware + route handler). If exceeded, the server responds with **503 Service Unavailable**. Omit or leave undefined for no timeout.
39+
40+
```typescript
41+
const router = new Router({
42+
routesDir: 'routes',
43+
requestTimeoutMs: 30_000
44+
})
45+
```
46+
3547
## Supported File Extensions
3648

3749
Deserve automatically detects and supports these file extensions:

docs/en/getting-started/server-configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ await router.serve(8000, '127.0.0.1')
5757
await router.serve(8000, '0.0.0.0')
5858
```
5959

60+
## Request Timeout
61+
62+
You can set a request timeout when creating the router. If middleware and route handler do not finish within that time, the server responds with **503 Service Unavailable**:
63+
64+
```typescript
65+
const router = new Router({
66+
requestTimeoutMs: 30_000
67+
})
68+
await router.serve(8000)
69+
```
70+
71+
Omit `requestTimeoutMs` for no timeout (default).
72+
6073
## Graceful Shutdown
6174

6275
Use `AbortSignal` for graceful server shutdown:

docs/en/middleware/body-limit.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **Reference**: [RFC 7230 HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1)
44
5-
Body Limit middleware enforces maximum request body size by checking the `Content-Length` header. Prevents large payloads from overwhelming your server.
5+
Body Limit middleware enforces maximum request body size. When a body is present, the body stream is always wrapped with a limiter so size is enforced regardless of headers. Prevents large payloads from overwhelming your server.
66

77
## Basic Usage
88

@@ -60,20 +60,16 @@ limit: 10 * 1024 * 1024
6060

6161
## How It Works
6262

63-
The middleware checks the `Content-Length` header before the body is read:
63+
When a request has a body, the middleware wraps the body stream with a byte limiter so the size is enforced as the body is read (not only via headers):
6464

65-
1. **GET/HEAD requests** - Automatically skipped (no body)
66-
2. **Content-Length present** - Validates against limit
67-
3. **Transfer-Encoding present** - Passes through (chunked encoding)
68-
4. **No headers** - Passes through (size unknown)
65+
1. **GET/HEAD or no body** - No wrapping; request passes through.
66+
2. **Body present** - Body stream is always wrapped with the limiter. If the client sends more bytes than `limit`, reading stops and the middleware responds with **413 Request Entity Too Large**.
67+
3. **Content-Length** - When present and above `limit`, the middleware may reject the request before reading the body (early reject).
6968

70-
### RFC 7230 Compliance
69+
### RFC 7230
7170

72-
The middleware follows RFC 7230:
73-
74-
- If both `Transfer-Encoding` and `Content-Length` are present, `Transfer-Encoding` takes precedence and body size is not validated
75-
- Only validates `Content-Length` when `Transfer-Encoding` is absent
76-
- Handles chunked encoding by passing through (can't check size upfront)
71+
- If both `Transfer-Encoding` and `Content-Length` are present, `Transfer-Encoding` takes precedence.
72+
- Chunked or unknown-length bodies are still limited by the wrapped stream; only the bytes read count toward the limit.
7773

7874
## Complete Example
7975

docs/en/middleware/global.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@ await router.serve(8000)
2626
## Middleware Function Signature
2727

2828
```typescript
29-
type Middleware = (ctx: Context, next: () => Promise<Response>) => Response | Promise<Response>
29+
type Middleware = (
30+
ctx: Context,
31+
next: () => Promise<Response | undefined>
32+
) => Response | Promise<Response | undefined>
3033
```
3134
32-
- **Return `await next()`** - Always called to continue to next middleware or route handler, allows response modification and inspection
33-
- **Return `Response`** - Stop processing and return response immediately
34-
- **Return `undefined`** - Pass through middleware (automatically calls `next()`)
35+
- **Return `await next()`** - Continue to next middleware or route handler; allows response modification and inspection.
36+
- **Return `Response`** - Stop processing and return that response immediately.
37+
- **Return `undefined`** - Treated as pass-through (chain continues as if `next()` were called).
38+
39+
Middleware must either call `next()` and use its result or return a `Response`. If it does neither (e.g. never calls `next()` and returns nothing), the request can hang; use `requestTimeoutMs` in `Router` to cap request duration and get a 503 instead.
3540
3641
## Common Global Middleware Patterns
3742

docs/en/middleware/session.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Session Middleware
22

3-
Session middleware stores session data in a cookie and exposes it via `ctx.state`, suitable for login, preferences, or per-user state without a session database.
3+
Session middleware stores session data in a signed cookie and exposes it via `ctx.state`, suitable for login, preferences, or per-user state without a session database. The cookie payload is signed with HMAC-SHA256; **`cookieSecret` is required**.
44

55
## Basic Usage
66

7-
Use `Mware.session()` to add cookie-based session:
7+
Use `Mware.session({ cookieSecret })` to add cookie-based session:
88

99
```typescript
1010
// 1. Import Router and Mware
@@ -13,17 +13,21 @@ import { Router, Mware } from '@neabyte/deserve'
1313
// 2. Create router
1414
const router = new Router()
1515

16-
// 3. Apply session middleware (cookie-based)
17-
router.use(Mware.session())
16+
// 3. Apply session middleware (cookieSecret required for signing)
17+
router.use(
18+
Mware.session({
19+
cookieSecret: Deno.env.get('SESSION_SECRET') ?? 'your-secret-min-32-chars'
20+
})
21+
)
1822

1923
// 4. Start server
2024
await router.serve(8000)
2125
```
2226

2327
After that, in route handlers or middleware:
2428

25-
- **`ctx.state.session`** - Session data (object or `null` if none yet)
26-
- **`ctx.state.setSession(data)`** - Save data to session (sets cookie)
29+
- **`ctx.state.session`** - Session data (object or `null` if none or invalid signature)
30+
- **`ctx.state.setSession(data)`** - Async; saves data to session (sets signed cookie). Use `await ctx.state.setSession(data)`.
2731
- **`ctx.state.clearSession()`** - Clear session (clear cookie)
2832

2933
## Example: Login And Logout
@@ -35,9 +39,10 @@ import type { Context } from '@neabyte/deserve'
3539
export async function POST(ctx: Context): Promise<Response> {
3640
// 1. Read JSON body (username, password)
3741
const body = await ctx.json()
38-
// 2. Check credentials; if valid, save to session
42+
// 2. Check credentials; if valid, save to session (setSession is async)
3943
if (body?.username === 'admin' && body?.password === 'secret') {
40-
ctx.state.setSession({ userId: '1', username: 'admin' })
44+
const setSession = ctx.state.setSession as (data: Record<string, unknown>) => Promise<void>
45+
await setSession({ userId: '1', username: 'admin' })
4146
return ctx.send.json({ ok: true })
4247
}
4348
// 3. Invalid → 401
@@ -65,12 +70,13 @@ export function DELETE(ctx: Context): Response {
6570

6671
## Session Options
6772

68-
You can change cookie name, max age, path, and security attributes:
73+
**`cookieSecret`** is required (used for HMAC-SHA256 signing). You can also set cookie name, max age, path, and security attributes:
6974

7075
```typescript
71-
// 1. Apply session with custom options
76+
// 1. Apply session with cookieSecret and custom options
7277
router.use(
7378
Mware.session({
79+
cookieSecret: Deno.env.get('SESSION_SECRET') ?? 'fallback-secret-min-32-chars',
7480
cookieName: 'sid',
7581
maxAge: 3600,
7682
path: '/',
@@ -80,15 +86,16 @@ router.use(
8086
)
8187
```
8288

83-
| Option | Default | Description |
84-
| ------------ | ----------- | ------------------------------------ |
85-
| `cookieName` | `'session'` | Cookie name |
86-
| `maxAge` | `86400` | Cookie age in seconds (24 hours) |
87-
| `path` | `'/'` | Cookie path |
88-
| `sameSite` | `'Lax'` | `'Strict' \| 'Lax' \| 'None'` |
89-
| `httpOnly` | `true` | Cookie not accessible from JavaScript |
89+
| Option | Default | Description |
90+
| -------------- | ----------- | -------------------------------------------- |
91+
| `cookieSecret` || **Required.** Secret for signing the cookie. |
92+
| `cookieName` | `'session'` | Cookie name |
93+
| `maxAge` | `86400` | Cookie age in seconds (24 hours) |
94+
| `path` | `'/'` | Cookie path |
95+
| `sameSite` | `'Lax'` | `'Strict' \| 'Lax' \| 'None'` |
96+
| `httpOnly` | `true` | Cookie not accessible from JavaScript |
9097

9198
## Limitations
9299

93-
- Session data is stored in the cookie (base64 + JSON). Do not store large or sensitive data; use only for identifiers or small data.
100+
- Session data is stored in the cookie and signed with HMAC-SHA256. Do not store large or highly sensitive data; use only for identifiers or small data.
94101
- For server-side or token-based session, use another mechanism (JWT, Redis, etc.) outside this middleware.

docs/id/getting-started/routes-configuration.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ Konfigurasi direktori routes Deserve agar sesuai dengan struktur proyek Anda.
44

55
## Opsi Router
66

7-
Konstruktor `Router` menerima opsi konfigurasi. Opsi utama yang sering dipakai adalah `routesDir` (direktori tempat file route Anda):
7+
Konstruktor `Router` menerima opsi konfigurasi. Opsi utama: `routesDir` (direktori file route), `requestTimeoutMs` (timeout request), serta opsional `errorResponseBuilder` / `staticHandler`.
88

99
```typescript
1010
// 1. Import Router
1111
import { Router } from '@neabyte/deserve'
1212

13-
// 2. Beri routesDir custom (default: ./routes)
13+
// 2. routesDir custom dan opsional request timeout (default: ./routes, tanpa timeout)
1414
const router = new Router({
15-
routesDir: 'src/routes'
15+
routesDir: 'src/routes',
16+
requestTimeoutMs: 30_000
1617
})
1718
```
1819

@@ -32,6 +33,17 @@ const router = new Router({
3233
})
3334
```
3435

36+
### `requestTimeoutMs`
37+
38+
Opsi timeout dalam milidetik untuk seluruh request (middleware + route handler). Jika terlampaui, server merespons **503 Service Unavailable**. Omit atau biarkan undefined untuk tanpa timeout.
39+
40+
```typescript
41+
const router = new Router({
42+
routesDir: 'routes',
43+
requestTimeoutMs: 30_000
44+
})
45+
```
46+
3547
## Ekstensi File Yang Didukung
3648

3749
Deserve secara otomatis mendeteksi dan mendukung ekstensi file ini:

docs/id/getting-started/server-configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ await router.serve(8000, '127.0.0.1')
5757
await router.serve(8000, '0.0.0.0')
5858
```
5959

60+
## Request Timeout
61+
62+
Anda bisa mengatur timeout request saat membuat router. Jika middleware dan route handler tidak selesai dalam waktu tersebut, server merespons **503 Service Unavailable**:
63+
64+
```typescript
65+
const router = new Router({
66+
requestTimeoutMs: 30_000
67+
})
68+
await router.serve(8000)
69+
```
70+
71+
Omit `requestTimeoutMs` untuk tanpa timeout (default).
72+
6073
## Graceful Shutdown
6174

6275
Gunakan `AbortSignal` untuk graceful server shutdown:

docs/id/middleware/body-limit.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **Referensi**: [RFC 7230 HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1)
44
5-
Middleware Body Limit menegakkan ukuran body request maksimum dengan memeriksa header `Content-Length`. Mencegah payload besar yang dapat membebani server Anda.
5+
Middleware Body Limit menegakkan ukuran body request maksimum. Jika body ada, stream body selalu dibungkus dengan limiter sehingga ukuran ditegakkan terlepas dari header. Mencegah payload besar yang dapat membebani server Anda.
66

77
## Penggunaan Dasar
88

@@ -60,20 +60,16 @@ limit: 10 * 1024 * 1024
6060

6161
## Cara Kerja Body Limit
6262

63-
Middleware memeriksa header `Content-Length` sebelum body dibaca:
63+
Jika request punya body, middleware membungkus stream body dengan byte limiter sehingga ukuran ditegakkan saat body dibaca (bukan hanya lewat header):
6464

65-
1. **Request GET/HEAD** - Secara otomatis dilewati (tidak ada body)
66-
2. **Content-Length ada** - Memvalidasi terhadap limit
67-
3. **Transfer-Encoding ada** - Melewati (chunked encoding)
68-
4. **Tidak ada header** - Melewati (ukuran tidak diketahui)
65+
1. **GET/HEAD atau tanpa body** - Tidak dibungkus; request dilewati.
66+
2. **Body ada** - Stream body selalu dibungkus dengan limiter. Jika klien mengirim byte lebih dari `limit`, pembacaan dihentikan dan middleware merespons **413 Request Entity Too Large**.
67+
3. **Content-Length** - Jika ada dan di atas `limit`, middleware bisa menolak request sebelum membaca body (early reject).
6968

70-
### Kepatuhan RFC 7230
69+
### RFC 7230
7170

72-
Middleware mengikuti RFC 7230:
73-
74-
- Jika `Transfer-Encoding` dan `Content-Length` keduanya ada, `Transfer-Encoding` memiliki prioritas dan ukuran body tidak divalidasi
75-
- Hanya memvalidasi `Content-Length` ketika `Transfer-Encoding` tidak ada
76-
- Menangani chunked encoding dengan melewati (tidak dapat memeriksa ukuran sebelumnya)
71+
- Jika `Transfer-Encoding` dan `Content-Length` keduanya ada, `Transfer-Encoding` diutamakan.
72+
- Body chunked atau ukuran tidak diketahui tetap dibatasi oleh stream yang dibungkus; hanya byte yang dibaca yang dihitung ke limit.
7773

7874
## Contoh Lengkap
7975

docs/id/middleware/global.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@ await router.serve(8000)
2626
## Signature Fungsi Middleware
2727

2828
```typescript
29-
type Middleware = (ctx: Context, next: () => Promise<Response>) => Response | Promise<Response>
29+
type Middleware = (
30+
ctx: Context,
31+
next: () => Promise<Response | undefined>
32+
) => Response | Promise<Response | undefined>
3033
```
3134
32-
- **Return `await next()`** - Selalu dipanggil untuk melanjutkan ke middleware atau route handler berikutnya, memungkinkan modifikasi dan inspeksi response
33-
- **Return `Response`** - Hentikan pemrosesan dan kembalikan response segera
34-
- **Return `undefined`** - Lewati middleware (otomatis memanggil `next()`)
35+
- **Return `await next()`** - Lanjut ke middleware atau route handler berikutnya; memungkinkan modifikasi dan inspeksi response.
36+
- **Return `Response`** - Hentikan pemrosesan dan kembalikan response tersebut.
37+
- **Return `undefined`** - Dianggap pass-through (rantai berlanjut seperti `next()` dipanggil).
38+
39+
Middleware harus memanggil `next()` dan memakai hasilnya atau mengembalikan `Response`. Jika tidak (mis. tidak pernah memanggil `next()` dan tidak return apa-apa), request bisa hang; gunakan `requestTimeoutMs` di `Router` untuk membatasi durasi request dan mendapat 503.
3540
3641
## Pola Middleware Global Umum
3742
@@ -67,7 +72,8 @@ router.use(async (ctx, next) => {
6772
if (!isValidToken(token)) {
6873
return ctx.send.text('Invalid token', { status: 401 })
6974
}
70-
// 4. Valid → lanjut ke next (return await next())
75+
// 4. Valid → lanjut
76+
return await next()
7177
})
7278
```
7379

0 commit comments

Comments
 (0)