@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 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
- A composable request-chaining and proxy engine for Next.js architectures with deterministic header and cookie accumulation, isolated request context, and a pure core design.
3
+ Composable request pipeline for Next.js middleware and proxy.
4
4
 
5
- ---
6
-
7
- ## Overview & Motivation
5
+ [![npm version](https://img.shields.io/npm/v/@klnap/next-proxy-chain)](https://www.npmjs.com/package/@klnap/next-proxy-chain)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@klnap/next-proxy-chain)](https://www.npmjs.com/package/@klnap/next-proxy-chain)
7
+ [![license](https://img.shields.io/npm/l/@klnap/next-proxy-chain)](https://github.com/klnap/next-proxy-chain/blob/main/LICENSE)
8
8
 
9
- In modern Next.js architectures, edge routing, backend-for-frontend (BFF) layers, multi-tenant resolution, and security policies frequently require multi-hop request processing. Standard monolithic middleware implementations lead to architectural challenges:
9
+ ---
10
10
 
11
- - **Nested Wrapper Hell**: Composing authentication, localization, rate limiting, and multi-tenant routing often results in deeply nested higher-order functions with fragile control flow.
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
- `next-proxy-chain` provides a linear, deterministic execution pipeline with zero framework-specific adapter boilerplate. It executes natively in both Next.js 15 (`middleware.ts`) and Next.js 16 (`proxy.ts`).
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
- # bun
30
- bun add @klnap/next-proxy-chain
31
+ Or with your preferred package manager:
31
32
 
32
- # pnpm
33
+ ```bash
33
34
  pnpm add @klnap/next-proxy-chain
34
-
35
- # yarn
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
- ## Core Architecture
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 requestIdProxy = defineProxy("requestId", (req) => {
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
- export const middleware = proxyChain([
113
- requestIdProxy,
114
- authProxy({ include: path("/dashboard/*") }),
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
- ### Next.js 16 Integration (`proxy.ts`)
75
+ ---
76
+
77
+ ## Routing & Scoping
123
78
 
124
- In Next.js 16, export the proxy chain as default or named `proxy` export:
79
+ Scope rules to specific paths, hosts, or combinations using `defineProxy`:
125
80
 
126
81
  ```typescript
127
- // proxy.ts
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 multiTenantProxy = defineProxy("multiTenant", (req, ctx) => {
132
- const hostname = req.headers.get("host") ?? ""
84
+ const apiAuth = defineProxy("apiAuth", async (req, ctx) => {
85
+ // Executed only for /api/* requests except /api/public/*
86
+ return next()
87
+ })
133
88
 
134
- if (hostname.startsWith("admin.")) {
135
- ctx.set("tenant", "admin")
136
- return next({
137
- request: { "x-tenant-id": "admin-portal" },
138
- })
139
- }
89
+ // Scoped rule configuration
90
+ const scopedRule = apiAuth({
91
+ include: path("/api/*"),
92
+ exclude: path("/api/public/*"),
93
+ })
140
94
 
141
- if (hostname.startsWith("app.")) {
142
- ctx.set("tenant", "app")
143
- return next({
144
- request: { "x-tenant-id": "customer-app" },
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 securityHeadersProxy = defineProxy("securityHeaders", () => {
152
- return next({
153
- headers: {
154
- "x-content-type-options": "nosniff",
155
- "x-frame-options": "DENY",
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({ debug: process.env.NODE_ENV !== "production" }, [
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 | Parameters | Return Type | Description |
173
- | :--- | :--- | :--- | :--- |
174
- | `proxyChain(rules)` | `Entry<TContext>[]` | `(req: NextRequest, event: NextFetchEvent) => Promise<NextResponse \| Response>` | Creates an execution pipeline from an array of rules. |
175
- | `proxyChain(options, rules)` | `ProxyChainOptions<TContext>, Entry<TContext>[]` | `(req: NextRequest, event: NextFetchEvent) => Promise<NextResponse \| Response>` | Creates an execution pipeline with custom configuration. |
176
- | `defineProxy(name?, fn)` | `string?, ProxyFn<TContext>` | `DefinedProxy<TContext>` | Creates a reusable rule with path and host scoping capabilities. |
177
- | `next(options?)` | `NextOptions?` | `NextResult` | Generates a pipeline continuation token with optional header mutations. |
178
- | `path(pattern, options?)` | `string \| RegExp, PathOptions?` | `RegExp` | Compiles a glob pattern, parameterized route, or regex into a stateless path matcher. |
179
- | `host(matcher)` | `HostMatcher` | `RegExp` | Compiles a single or list of hostnames/patterns into an anchored host matcher. |
180
- | `createProxyContext()` | `None` | `ProxyContext` | Instantiates a new Map instance for isolated request context storage. |
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: NextRequest, event: NextFetchEvent) => TContext` | `() => new Map()` | Custom factory function for request context instantiation. |
187
- | `cookieMergeStrategy` | `MergeStrategy` | `"last-write-wins"` | Strategy for resolving conflicting `Set-Cookie` directives across rules. |
188
- | `headerMergeStrategy` | `MergeStrategy` | `"last-write-wins"` | Strategy for resolving conflicting response headers. |
189
- | `debug` | `boolean` | `false` | Enables structured single-line execution logs for each rule. |
190
- | `ruleTimeoutMs` | `number` | `0` | Wall-clock execution budget per rule in milliseconds (`0` disables timeout). |
191
- | `requestHeaderPolicy` | `RequestHeaderPolicy` | `{}` | Custom policy defining blocked and allowed request header mutations. |
192
- | `strictOverrideCheck` | `boolean` | `false` | Throws an exception at initialization if platform request overrides are unsupported. |
193
- | `onError` | `(err, index, req, ctx) => Response \| undefined` | `undefined` | Hook invoked when a rule throws an exception to allow custom error recovery. |
194
-
195
- ### Rule Scoping Options (`ProxyPathFilter`)
196
-
197
- | Property | Type | Description |
198
- | :--- | :--- | :--- |
199
- | `include` | `RouteScopeList` | Routes or hosts where the rule must execute. |
200
- | `exclude` | `RouteScopeList` | Routes or hosts where the rule must be skipped (takes precedence over `include`). |
201
- | `name` | `string` | Human-readable identifier used for logs and diagnostic metrics. |
202
- | `pathOptions` | `PathOptions` | Route compilation options, such as locale prefixes (`locales`). |
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` rule function must return one of the following:
217
+ A rule (`ProxyFn`) can return:
207
218
 
208
- | Return Value | Pipeline Behavior |
219
+ | Return Value | Behavior |
209
220
  | :--- | :--- |
210
- | `next({ request?, headers? })` | Continues execution, staging request header mutations and response headers. |
211
- | `undefined` or `void` | Continues execution without mutating headers or state. |
212
- | `NextResponse.next()` | Continues execution while extracting any internal request header overrides. |
213
- | `NextResponse.rewrite(url)` | Continues execution while setting the rewrite target for downstream rules. |
214
- | `NextResponse.redirect(url)` | Halts pipeline execution immediately and returns the redirect response. |
215
- | `NextResponse.json(...)` or `Response` | Halts pipeline execution immediately and returns the terminal response. |
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
+