@klnap/next-proxy-chain 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +165 -146
- package/dist/index.cjs +335 -320
- package/dist/index.d.cts +65 -169
- package/dist/index.d.ts +65 -169
- package/dist/index.js +334 -289
- package/package.json +19 -12
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@klnap/next-proxy-chain` will be documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.4] - 2026-08-27
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
- `onError` returning `undefined` / `void` / `null` now **fails closed** with a 500. Continue only by returning `next()`.
|
|
14
|
+
- `x-internal-host-folder` is no longer a magic routing header. Enable it with `hostFolderHeader: "x-internal-host-folder"`.
|
|
15
|
+
- Public exports are limited to the documented API. Internals (`mergeWrites`, `compileScopes`, `withRuleTimeout`, `./core`, …) are no longer exported.
|
|
16
|
+
- Custom `TContext` requires `createContext`. `proxyChain<{ user: string }>(rules)` without a factory is a type error.
|
|
17
|
+
- Removed `PathOptions` and `pathOptions`; `path()` no longer accepts a `locales` option. Match locale-prefixed routes in the pattern string or RegExp.
|
|
18
|
+
- `ruleTimeoutMs`: passing `NaN`, `Infinity`, or `-Infinity` now throws a `RangeError` at proxy-chain configuration time instead of silently disabling the timeout. If you were relying on an unvalidated computed value (e.g. from `parseInt` without a fallback) resulting in a disabled timeout, this is a breaking change — validate your config value before passing it.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `path()` no longer drops `^` / `$` from user RegExp.
|
|
23
|
+
- Trailing `*` matches exactly one segment; `**` matches nested paths.
|
|
24
|
+
- `compileScopes()` runs RegExp matchers through `path()`.
|
|
25
|
+
- `next({ headers: { "set-cookie": … } })` reaches the final response.
|
|
26
|
+
- Cookies with the same name and different `Path` / `Domain` are not collapsed.
|
|
27
|
+
- Custom header merge returning `null` deletes the header from the terminal response.
|
|
28
|
+
- Rewrite continuation no longer mutates the incoming `NextRequest`.
|
|
29
|
+
- Timed-out rules no longer leave an unhandled rejection.
|
|
30
|
+
- `host("*.example.com")` matches one DNS label.
|
|
31
|
+
- `strictOverrideCheck` cache no longer pins the first call’s strictness for later calls.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `headerMergeStrategy` applies only to **response** headers. Request overrides stay last-write-wins.
|
|
36
|
+
- Package metadata: `repository`, `bugs`, `homepage`.
|
|
37
|
+
- Tests run on `bun:test`; removed Typedoc site and monorepo tooling from the package repo.
|
|
38
|
+
|
|
39
|
+
## [1.0.3] - 2026-08-26
|
|
40
|
+
|
|
41
|
+
npm patch. No detailed notes were kept for 1.0.1–1.0.3 at publish time.
|
|
42
|
+
|
|
43
|
+
## [1.0.0] - 2026-08-23
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Composable Edge-safe proxy pipeline via `proxyChain()`.
|
|
48
|
+
- Transparent Next.js pass-through detection on `x-middleware-next: 1` with request override extraction.
|
|
49
|
+
- Automatic filtering of internal `x-middleware-*` transport headers from client responses.
|
|
50
|
+
- Request header blocklist preventing rule injection of sensitive headers (`host`, `authorization`, `x-middleware-*`).
|
|
51
|
+
- Context threading via isolated per-request `Map` instances (`TContext`).
|
|
52
|
+
- Header and cookie accumulation with `last-write-wins`, `first-write-wins`, and custom merge strategies.
|
|
53
|
+
- Path and host scoping (`include`, `exclude`) with construction-time overlap validation.
|
|
54
|
+
- Cached startup self-test for `NextResponse.next()` request overrides.
|
|
55
|
+
- Error isolation with safe 500 fallback.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 klnap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,117 +1,70 @@
|
|
|
1
1
|
# @klnap/next-proxy-chain
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable request pipeline for Next.js middleware and proxy.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@klnap/next-proxy-chain)
|
|
6
|
+
[](https://www.npmjs.com/package/@klnap/next-proxy-chain)
|
|
7
|
+
[](https://github.com/klnap/next-proxy-chain/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- **Header Mutation Conflicts**: Independent middleware steps often overwrite request headers (`x-middleware-request-*`) or drop previously injected values.
|
|
13
|
-
- **Cookie Directive Overwrites**: Multiple intermediate steps setting `Set-Cookie` directives risk clobbering each other or emitting duplicate headers that CDNs and browsers handle unpredictably.
|
|
14
|
-
- **Stateful Concurrency Hazards**: Shared state or stateful regular expressions (such as `RegExp` instances with global `g` or sticky `y` flags) cause non-deterministic route matching under high concurrency in Edge worker runtimes.
|
|
15
|
-
- **Header Injection Vulnerabilities**: Untrusted incoming requests can attempt to manipulate protected transport headers such as `Host`, `Authorization`, or Next.js internal headers (`x-middleware-*`).
|
|
11
|
+
## Features
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
- **Linear Composition**: Chain independent rules sequentially into a single Next.js middleware or proxy handler.
|
|
14
|
+
- **Next.js 15 & 16**: Supports both `middleware.ts` and `proxy.ts`.
|
|
15
|
+
- **Request-Scoped Context**: Isolated per-request context for sharing state between rules without global state.
|
|
16
|
+
- **Deterministic Header & Cookie Merging**: Propagate request header overrides downstream, deduplicate `Set-Cookie` headers, and merge response headers with configurable conflict strategies.
|
|
17
|
+
- **Route & Host Scoping**: Declarative path matching (globs, parameters, regex) and host filtering with `include` and `exclude` rules.
|
|
18
|
+
- **Safe Pattern Matching**: Normalizes stateful regular expressions to avoid `lastIndex`-related matching issues.
|
|
19
|
+
- **Explicit Flow Control**: Return `next({ request, headers })` to continue, or a terminal `Response` / `NextResponse.redirect` / `NextResponse.json` to stop. `NextResponse.rewrite` **continues** the pipeline (rewrite is applied at the end unless a later rule stops).
|
|
20
|
+
- **Timeouts & Error Recovery**: Optional per-rule timeouts (`ruleTimeoutMs`). `onError` is fail-closed: returning nothing yields 500; return `next()` to continue; return a `Response` to stop.
|
|
21
|
+
- **Zero Runtime Dependencies**: Fully typed in TypeScript with no runtime dependencies (`next` is a peer).
|
|
18
22
|
|
|
19
23
|
---
|
|
20
24
|
|
|
21
25
|
## Installation
|
|
22
26
|
|
|
23
|
-
Install the package via your preferred package manager:
|
|
24
|
-
|
|
25
27
|
```bash
|
|
26
|
-
# npm
|
|
27
28
|
npm install @klnap/next-proxy-chain
|
|
29
|
+
```
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
bun add @klnap/next-proxy-chain
|
|
31
|
+
Or with your preferred package manager:
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
```bash
|
|
33
34
|
pnpm add @klnap/next-proxy-chain
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
# or
|
|
36
|
+
bun add @klnap/next-proxy-chain
|
|
37
|
+
# or
|
|
36
38
|
yarn add @klnap/next-proxy-chain
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
---
|
|
40
42
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
`next-proxy-chain` orchestrates incoming requests through a linear pipeline of isolated rules:
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
[Incoming Request]
|
|
47
|
-
│
|
|
48
|
-
▼
|
|
49
|
-
┌──────────────┐
|
|
50
|
-
│ createContext│ ───> Map<string, unknown> (Isolated per request)
|
|
51
|
-
└──────┬───────┘
|
|
52
|
-
│
|
|
53
|
-
▼
|
|
54
|
-
┌──────────────┐ (Matched by path/host filter)
|
|
55
|
-
│ Rule 1 │ ───> return next({ request, headers })
|
|
56
|
-
└──────┬───────┘
|
|
57
|
-
│ Materializes mutated NextRequest only when next rule executes
|
|
58
|
-
▼
|
|
59
|
-
┌──────────────┐ (Matched by path/host filter)
|
|
60
|
-
│ Rule 2 │ ───> return next() | NextResponse.redirect()
|
|
61
|
-
└──────┬───────┘
|
|
62
|
-
│
|
|
63
|
-
▼
|
|
64
|
-
┌──────────────┐
|
|
65
|
-
│ Final Merge │ ───> Merges all request overrides, response headers, & cookies
|
|
66
|
-
└──────┬───────┘
|
|
67
|
-
│
|
|
68
|
-
▼
|
|
69
|
-
[Outgoing NextResponse / Response]
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Key Architectural Properties
|
|
73
|
-
|
|
74
|
-
1. **Pure Core**: The underlying rule engine operates strictly on Web Standards (`Request`, `Response`, `Headers`, `URL`, `Map`).
|
|
75
|
-
2. **Deterministic Conflict Resolution**: Outgoing response headers and cookies are merged according to configurable strategies (`last-write-wins`, `first-write-wins`, or custom resolvers).
|
|
76
|
-
3. **Lazy Request Materialization**: Cloned `NextRequest` objects are only instantiated when downstream rules read mutated headers, minimizing memory allocations.
|
|
77
|
-
4. **Edge Runtime Hardening**: Strips `g` and `y` flags from regular expressions to prevent race conditions during concurrent request processing.
|
|
78
|
-
5. **Fail-Safe Recovery**: Unhandled rule exceptions are caught and routed through optional `onError` handlers, falling back to a safe HTTP 500 response.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Integration Guide
|
|
83
|
-
|
|
84
|
-
### Next.js 15 Integration (`middleware.ts`)
|
|
85
|
-
|
|
86
|
-
In Next.js 15, export the proxy chain directly as the middleware handler:
|
|
43
|
+
## Quick Start
|
|
87
44
|
|
|
88
45
|
```typescript
|
|
89
|
-
// middleware.ts
|
|
46
|
+
// proxy.ts or middleware.ts
|
|
90
47
|
import { defineProxy, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
91
48
|
import { NextResponse } from "next/server"
|
|
92
49
|
|
|
93
|
-
const
|
|
94
|
-
const id = req.headers.get("x-request-id") ?? crypto.randomUUID()
|
|
95
|
-
return next({
|
|
96
|
-
request: { "x-request-id": id },
|
|
97
|
-
headers: { "x-request-id": id },
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const authProxy = defineProxy("auth", async (req, ctx) => {
|
|
50
|
+
const auth = defineProxy("auth", (req, ctx) => {
|
|
102
51
|
const token = req.cookies.get("session")?.value
|
|
103
|
-
|
|
104
52
|
if (!token) {
|
|
105
53
|
return NextResponse.redirect(new URL("/login", req.url))
|
|
106
54
|
}
|
|
107
|
-
|
|
108
|
-
ctx.set("userId", "usr_100")
|
|
55
|
+
ctx.set("userId", "usr_123")
|
|
109
56
|
return next()
|
|
110
57
|
})
|
|
111
58
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
59
|
+
const security = defineProxy("security", () => {
|
|
60
|
+
return next({
|
|
61
|
+
headers: { "x-frame-options": "DENY" },
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default proxyChain({ debug: process.env.NODE_ENV !== "production" }, [
|
|
66
|
+
auth({ include: path("/dashboard/*") }),
|
|
67
|
+
security,
|
|
115
68
|
])
|
|
116
69
|
|
|
117
70
|
export const config = {
|
|
@@ -119,103 +72,169 @@ export const config = {
|
|
|
119
72
|
}
|
|
120
73
|
```
|
|
121
74
|
|
|
122
|
-
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Routing & Scoping
|
|
123
78
|
|
|
124
|
-
|
|
79
|
+
Scope rules to specific paths, hosts, or combinations using `defineProxy`:
|
|
125
80
|
|
|
126
81
|
```typescript
|
|
127
|
-
|
|
128
|
-
import { defineProxy, host, next, path, proxyChain } from "@klnap/next-proxy-chain"
|
|
129
|
-
import { NextResponse } from "next/server"
|
|
82
|
+
import { defineProxy, host, next, path } from "@klnap/next-proxy-chain"
|
|
130
83
|
|
|
131
|
-
const
|
|
132
|
-
|
|
84
|
+
const apiAuth = defineProxy("apiAuth", async (req, ctx) => {
|
|
85
|
+
// Executed only for /api/* requests except /api/public/*
|
|
86
|
+
return next()
|
|
87
|
+
})
|
|
133
88
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
89
|
+
// Scoped rule configuration
|
|
90
|
+
const scopedRule = apiAuth({
|
|
91
|
+
include: path("/api/*"),
|
|
92
|
+
exclude: path("/api/public/*"),
|
|
93
|
+
})
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
95
|
+
// Combined path and host filter
|
|
96
|
+
const adminRule = apiAuth({
|
|
97
|
+
include: {
|
|
98
|
+
host: host("admin.example.com"),
|
|
99
|
+
path: path("/settings/**"),
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Path Patterns
|
|
105
|
+
|
|
106
|
+
- `path("/api/*")` — exactly one segment (`/api/users` yes, `/api/users/123` no)
|
|
107
|
+
- `path("/docs/**")` — the prefix plus zero or more extra segments (`/docs`, `/docs/a/b`)
|
|
108
|
+
- `path("/users/:id")` — one segment (no capture groups; matching only)
|
|
109
|
+
- `path(/^\/foo$/)` — user anchors are kept (`/foo` yes, `/x/foo` no)
|
|
110
|
+
|
|
111
|
+
### Host Patterns
|
|
112
|
+
|
|
113
|
+
- `host("admin.example.com")` — exact hostname (case-insensitive, port stripped at match time)
|
|
114
|
+
- `host("*.example.com")` — one DNS label (`foo.example.com` yes, `example.com` no, `a.b.example.com` no)
|
|
115
|
+
- `host(/shop\.localhost$/)` — raw RegExp
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## State & Header Mutation
|
|
120
|
+
|
|
121
|
+
### Request Context
|
|
147
122
|
|
|
123
|
+
Default context is an isolated `Map<string, unknown>` per request. For a typed object, pass `createContext` — TypeScript then follows the factory, not a fake generic:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
type Ctx = { user?: { id: string; role: string } }
|
|
127
|
+
|
|
128
|
+
const enrichUser = defineProxy<Ctx>("enrichUser", async (req, ctx) => {
|
|
129
|
+
ctx.user = { id: "usr_1", role: "admin" }
|
|
148
130
|
return next()
|
|
149
131
|
})
|
|
150
132
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
},
|
|
157
|
-
})
|
|
133
|
+
const checkPermission = defineProxy<Ctx>("checkPermission", (req, ctx) => {
|
|
134
|
+
if (ctx.user?.role !== "admin") {
|
|
135
|
+
return new Response("Forbidden", { status: 403 })
|
|
136
|
+
}
|
|
137
|
+
return next()
|
|
158
138
|
})
|
|
159
139
|
|
|
160
|
-
export default proxyChain({
|
|
161
|
-
multiTenantProxy,
|
|
162
|
-
securityHeadersProxy,
|
|
163
|
-
])
|
|
140
|
+
export default proxyChain({ createContext: (): Ctx => ({}) }, [enrichUser, checkPermission])
|
|
164
141
|
```
|
|
165
142
|
|
|
143
|
+
Without `createContext`, `ctx` is `ProxyContext` (`Map`). Do not write `proxyChain<{ user: string }>(rules)` — that is a type error.
|
|
144
|
+
|
|
145
|
+
### Request and Response Headers
|
|
146
|
+
|
|
147
|
+
Use `next()` to inject **request** headers (visible to downstream rules and route handlers) and **response** headers (including `Set-Cookie`):
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
return next({
|
|
151
|
+
// Downstream request headers (NextRequest & server components). Always last-write-wins.
|
|
152
|
+
request: {
|
|
153
|
+
"x-user-id": "usr_123",
|
|
154
|
+
},
|
|
155
|
+
// Final outgoing response headers / Set-Cookie
|
|
156
|
+
headers: {
|
|
157
|
+
"cache-control": "no-store",
|
|
158
|
+
"set-cookie": "session=abc; Path=/",
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `headerMergeStrategy` / `cookieMergeStrategy` apply to **response** headers and cookies only.
|
|
164
|
+
- Request overrides ignore those strategies (last write wins).
|
|
165
|
+
- Cookies are keyed by **name + domain + path**, so `session; Path=/` and `session; Path=/admin` both survive.
|
|
166
|
+
- Custom merge `return null` removes that response header from the final `Response`.
|
|
167
|
+
|
|
166
168
|
---
|
|
167
169
|
|
|
168
170
|
## API Reference
|
|
169
171
|
|
|
170
172
|
### Core Functions
|
|
171
173
|
|
|
172
|
-
| Function |
|
|
173
|
-
| :--- | :--- | :--- |
|
|
174
|
-
| `proxyChain(rules)` | `Entry
|
|
175
|
-
| `proxyChain(options, rules)` | `
|
|
176
|
-
| `defineProxy(name?, fn)` | `string
|
|
177
|
-
| `next(options?)` | `NextOptions
|
|
178
|
-
| `path(pattern
|
|
179
|
-
| `host(matcher)` | `HostMatcher
|
|
180
|
-
| `createProxyContext()` | `
|
|
174
|
+
| Function | Signature | Description |
|
|
175
|
+
| :--- | :--- | :--- |
|
|
176
|
+
| `proxyChain(rules)` | `(rules: Entry[]) => Handler` | Creates a middleware/proxy handler from a list of rules. |
|
|
177
|
+
| `proxyChain(options, rules)` | `(options: ProxyChainOptions, rules: Entry[]) => Handler` | Creates a handler with custom pipeline options. |
|
|
178
|
+
| `defineProxy(name?, fn)` | `(name?: string, fn: ProxyFn) => DefinedProxy` | Defines a reusable proxy rule with scoping capabilities. |
|
|
179
|
+
| `next(options?)` | `(options?: NextOptions) => NextResult` | Returns a continuation token with optional header mutations. |
|
|
180
|
+
| `path(pattern)` | `(pattern: string \| RegExp) => RegExp` | Compiles a path pattern into a stateless `RegExp`. |
|
|
181
|
+
| `host(matcher)` | `(matcher: HostMatcher) => RegExp` | Compiles hostname(s) into an anchored `RegExp` (`*` = one DNS label). |
|
|
182
|
+
| `createProxyContext()` | `() => ProxyContext` | Creates a new `Map<string, unknown>` context instance. |
|
|
183
|
+
| `isNextResult(value)` | `(value: unknown) => boolean` | Type guard for `next()` continuation tokens. |
|
|
184
|
+
| `RuleTimeoutError` | `Error` | Thrown internally when `ruleTimeoutMs` is exceeded (catchable in `onError`). |
|
|
181
185
|
|
|
182
186
|
### Configuration Options (`ProxyChainOptions`)
|
|
183
187
|
|
|
184
188
|
| Option | Type | Default | Description |
|
|
185
189
|
| :--- | :--- | :--- | :--- |
|
|
186
|
-
| `createContext` | `(req
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `requestHeaderPolicy` | `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
| `
|
|
200
|
-
|
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
190
|
+
| `createContext` | `(req, event) => TContext` | `() => new Map()` | Factory for custom per-request context. Required for a non-`Map` `TContext`. |
|
|
191
|
+
| `headerMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Conflict strategy for **response** headers only. |
|
|
192
|
+
| `cookieMergeStrategy` | `"last-write-wins" \| "first-write-wins" \| Function` | `"last-write-wins"` | Conflict strategy for `Set-Cookie` (same name+domain+path). |
|
|
193
|
+
| `ruleTimeoutMs` | `number` | `0` | Wall-clock timeout per rule in ms (`0` disables). Does not abort the rule function; leftover rejections are swallowed. |
|
|
194
|
+
| `onError` | `(err, index, req, ctx) => Response \| NextResult \| void` | `undefined` | Recovery hook. See **Errors** below. |
|
|
195
|
+
| `requestHeaderPolicy` | `{ blocked?: string[], allowed?: string[] }` | `{}` | Blocklist/allowlist for **request** header mutations. |
|
|
196
|
+
| `debug` | `boolean` | `false` | Structured one-line logs per rule. |
|
|
197
|
+
| `logger` | `ChainLogger` | `console` | Custom logger (`debug`, `info`, `warn`, `error`). |
|
|
198
|
+
| `strictOverrideCheck` | `boolean` | `false` | Throw at init if `NextResponse.next()` request overrides look unsupported. |
|
|
199
|
+
| `hostFolderHeader` | `string` | unset | Opt-in: request header name that prefixes rewrites with a physical folder (e.g. `"x-internal-host-folder"`). Off by default. |
|
|
200
|
+
|
|
201
|
+
### Errors (`onError`)
|
|
202
|
+
|
|
203
|
+
| `onError` returns | Behavior |
|
|
204
|
+
| :--- | :--- |
|
|
205
|
+
| `Response` | Stop and return that response (headers/cookies still merged). |
|
|
206
|
+
| `next()` | Continue to the next rule (rule-level errors only). |
|
|
207
|
+
| `undefined` / `void` / `null` | **Fail closed** — `500 Internal Server Error`. |
|
|
208
|
+
| throws | `500 Internal Server Error`. |
|
|
209
|
+
| omitted | Rule throw → `500`. |
|
|
210
|
+
|
|
211
|
+
`ruleIndex` is `0`-based. `-1` means an engine-level error after context creation; only a `Response` is honored (the pipeline cannot continue).
|
|
212
|
+
|
|
213
|
+
A logging-only `onError: (err) => { console.error(err) }` does **not** skip the failure.
|
|
203
214
|
|
|
204
215
|
### Return Value Protocol
|
|
205
216
|
|
|
206
|
-
A `ProxyFn`
|
|
217
|
+
A rule (`ProxyFn`) can return:
|
|
207
218
|
|
|
208
|
-
| Return Value |
|
|
219
|
+
| Return Value | Behavior |
|
|
209
220
|
| :--- | :--- |
|
|
210
|
-
| `next({ request?, headers? })` | Continues
|
|
211
|
-
| `undefined`
|
|
212
|
-
| `NextResponse.next()` | Continues
|
|
213
|
-
| `NextResponse.rewrite(url)` | Continues
|
|
214
|
-
| `NextResponse.redirect(url)` |
|
|
215
|
-
| `NextResponse.json(
|
|
221
|
+
| `next({ request?, headers? })` | Continues. Queues request overrides, response headers, and `Set-Cookie`. |
|
|
222
|
+
| `undefined` / `void` | Continues without mutations. |
|
|
223
|
+
| `NextResponse.next()` | Continues; extracts Next.js request override headers and cookies. |
|
|
224
|
+
| `NextResponse.rewrite(url)` | **Continues**. Stages a rewrite for later rules; the incoming `NextRequest` is not mutated. The rewrite is applied when the chain finishes unless a later rule returns a terminal response. |
|
|
225
|
+
| `NextResponse.redirect(url)` | Stops immediately with the redirect. |
|
|
226
|
+
| `Response` / `NextResponse.json()` | Stops immediately with that response. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Links
|
|
231
|
+
|
|
232
|
+
- [GitHub](https://github.com/klnap/next-proxy-chain)
|
|
233
|
+
- [npm](https://www.npmjs.com/package/@klnap/next-proxy-chain?activeTab=code)
|
|
216
234
|
|
|
217
235
|
---
|
|
218
236
|
|
|
219
237
|
## License
|
|
220
238
|
|
|
221
|
-
MIT
|
|
239
|
+
[MIT](./LICENSE)
|
|
240
|
+
|