@12-apps/request-scope

中文文档
2.1.0发布于 2026/08/25MITES Module零依赖构建来源已验证近 30 天下载 3.2万

Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the

原始使用文档
文档目录10 节
  1. The problem it solves
  2. The three entry points
  3. Four decisions worth knowing about
  4. The read and write halves of a cookie ship as one object
  5. Encoding is a knob, and its default is the safe direction
  6. requireRequestScope() throws, deliberately
  7. A redirect gets rebuilt rather than forbidden
  8. The Hono adapter and c.res
  9. Mount order
  10. Coexisting with another AsyncLocalStorage

@12-apps/request-scope

The ambient per-request scope for a host serving web-standard Request/Response: read the incoming headers and cookies from anywhere in the call chain, queue a cookie write from code that has no response in hand, and have both merged onto the answer on the way out.

Zero runtime dependencies. node:async_hooks and nothing else.


The problem it solves

A web-standard handler takes a Request and returns a Response. That is a clean contract right up to the moment something twelve calls deep needs to know who is calling — a cart keyed by a cookie, a consent gate, an impersonation marker, an incoming bearer. There are two ways out:

  1. Thread the Request through every intervening signature. This turns a transport detail into a parameter of the domain layer, and it is viral: one new cookie reader changes a dozen function signatures and their tests.
  2. An ambient accessor, backed by an explicit store.

This package is the second, and the store is the point. AsyncLocalStorage is easy to reach for and easy to get subtly wrong — the store identity across a hot reload, the write half that has nowhere to write, the redirect whose headers refuse a Set-Cookie. Those are the parts worth sharing.

The three entry points

ImportFor
@12-apps/request-scopethe framework-free core — the store, the codec, the response helpers
@12-apps/request-scope/honothe Hono middleware (hono is an optional peer)
@12-apps/request-scope/next-compatcookies() / headers() with next/headers' shapes, for a host migrating off the App Router
import { requestScope } from '@12-apps/request-scope/hono';

app.use('*', requestScope());

That is the whole wiring. Then, anywhere downstream:

import { requireRequestScope, writeCookie } from '@12-apps/request-scope';

const scope = requireRequestScope();
const cartId = scope.values.get('cart');
writeCookie(scope, 'cart', nextId, { httpOnly: true, sameSite: 'lax', path: '/' });

A host that dispatches its own route table rather than composing middleware uses the one-call form instead:

import { serveWithRequestScope } from '@12-apps/request-scope';

const response = await serveWithRequestScope(request, () => handler(request));

Four decisions worth knowing about

createCookieCodec() hands back serialize, serializeDeletion and parse together. The only way a cookie layer can be wrong is by disagreeing with itself — a parser that percent-decodes paired with a serializer that does not encode round-trips everything it was tested with and mangles the first value containing a delimiter. Binding the pair to one object removes the call site that could import half of it.

Encoding is a knob, and its default is the safe direction

RFC 6265 forbids ;, ,, whitespace and control characters in a value, so percent-encoding is correct and is the default. But a host adopting this package already has cookies sitting in browsers, written by whatever it used before.

The two formats meet during a rollout, and only one direction survives:

  • a raw value read by the decoding parser is fine — decodeURIComponent is the identity on anything with no % in it, and a malformed escape falls back to the raw text rather than throwing;
  • an encoded value read by a raw parser is not, and that reader is the host's old deployed code, which this package cannot reach.

So createCookieCodec({ encode: false }) exists for hosts with existing raw cookies, and the flag flips once nothing parses those cookies by hand.

requireRequestScope() throws, deliberately

Code reading an optional credential — a bearer header, an impersonation cookie — wraps the accessor in a try and treats a throw as "there is no incoming request, so there is no credential". Returning undefined would collapse that into the same answer as "a request with no such cookie", and those need different handling: one is a background job, the other is an anonymous visitor.

currentRequestScope() is the non-throwing form, for code that legitimately runs both inside and outside a request.

A redirect gets rebuilt rather than forbidden

Response.redirect() is specified to return an immutable header list, so appending a Set-Cookie to it throws. Redirecting while clearing a cookie is completely ordinary (an OAuth callback dropping its state cookie), so rather than ban the built-in — a rule the next handler rediscovers as a 500 — applyResponseCookies rebuilds the response when its headers refuse the write.

redirectResponse() is the other half: a redirect built through the constructor, so its headers stay mutable. It defaults to 307, not the 302 Response.redirect() gives, because 307 preserves the method — a redirect added to a non-GET handler later behaves as written instead of silently downgrading the follow-up to a GET and losing the body.


The Hono adapter and c.res

Worth surfacing, because it is invisible until it bites. Hono's c.res setter re-merges headers from the previous response, and its set-cookie branch reads:

const cookies = this.#res.headers.getSetCookie();  // the OLD list
_res.headers.delete('set-cookie');                 // wipes the NEW list
for (const cookie of cookies) _res.headers.append('set-cookie', cookie);

So merging by assignment would drop the cookies this middleware queued — precisely and only when the handler wrote one of its own, which is the case where both must survive. The adapter appends in place instead, and falls back to assignment only for an immutable response, which by construction cannot be carrying a Set-Cookie of its own. Both halves are pinned by tests.

Mount order

Mount the middleware before anything that reads a cookie or header ambiently. A route registered above it runs outside the scope and its accessors throw — which is the intended failure: loud, at the first request, rather than a silently absent session.

Coexisting with another AsyncLocalStorage

This store is kept on globalThis so a dev server that re-evaluates the module cannot create a second one invisible to closures captured against the first. The key it lives under is configurable, for the same reason @12-apps/audit makes its actor-store key configurable: a host that already has an in-house request-scope module, with call sites importing it, needs both modules on one store.

import { declareRequestScopeKey } from '@12-apps/request-scope';

declareRequestScopeKey('__myHostRequestStore'); // at wiring time, once

Two stores that disagree do not fail loudly — the accessors read a store nothing ever entered and throw "outside a request scope" from inside a perfectly ordinary request. Calling this after a store exists is refused rather than silently honoured.

Note that the actor context in @12-apps/audit is a different scope with a different lifetime: it carries who is acting, this one carries what arrived. They are opened by the same adapter and otherwise have nothing to say to each other.


See ADOPTING.md for the integration playbook.

版本与发布记录

@12-apps/request-scope 共发布 4 个版本,首次发布于 2026/08/15;过去 12 个月发布了 4 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.1.0latest2026/08/252 个月前
2.0.12026/08/212 个月前
2.0.02026/08/172 个月前
1.0.02026/08/152 个月前

关于 @12-apps/request-scope 的常见问题

@12-apps/request-scope 是什么?

Generic, portable AMBIENT REQUEST SCOPE for hosts serving web-standard Request/Response. Framework-free core (an AsyncLocalStorage-backed per-request store, a paired cookie read/write codec, queued cookie writes drained onto the outgoing response, and the

如何安装 @12-apps/request-scope?

在项目目录运行 npm install @12-apps/request-scope,或使用 pnpm add / yarn add / bun add。

@12-apps/request-scope 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/12-apps__request-scope 是否存在。

@12-apps/request-scope 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@12-apps/request-scope 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@12-apps/request-scope 可以商用吗?

@12-apps/request-scope 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@12-apps/request-scope 的下载量有多少?

根据 npm 官方下载统计,@12-apps/request-scope 在 2026/10/04 – 2026/11/02 这 30 天内被下载 31,988 次,日均约 1,066 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@12-apps/request-scope 还在维护吗?

最新版本 2.1.0 发布于 2026/08/25(2 个月前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/08/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。