Summary
Next.js Server Actions POST to the page URL and carry the operation in the next-action header (the action id), with an RSC payload as the body (Content-Type: text/plain, Accept: text/x-component, plus a next-router-state-tree header). The normalizer has no handling for this, so each action is treated as a generic POST and keyed by body content/shape — fragmenting one operation across multiple endpoint rows, and frequently failing normalization outright because the RSC body/headers break the pipeline's assumptions.
Root cause — a missing case in an abstraction that already exists
src/session/normalize/protocol.ts already models exactly this class. Its own comment:
"For body/header-dispatched protocols (GraphQL, JSON-RPC) the real 'endpoint' is the OPERATION carried in the body, not the URL."
and exposes OperationInfo { protocol, operation, opKeyHash } with values stripped. But protocol is typed "graphql" | "jsonrpc". Next.js Server Actions are the same class (header-dispatched; next-action = the operation id) but are not a recognized protocol, so they fall through to generic POST normalization. As a result they are never assigned a protocol/operation/op_key_hash, and keying by body shape is both fragile (same action fragments when its body shape varies; different actions merge when shapes collide) and failure-prone.
Fix
Add a next-action branch to the protocol detector:
- Detect the
next-action request header (optionally corroborated by Accept: text/x-component).
- Emit
protocol: "nextjs-action", operation: <action-id> (a stable per-action hash), opKeyHash = sha16(canonicalPath + ":" + <action-id>).
- Strip the RSC
text/plain body and the next-router-state-tree header from the key (they are transport/router state, not endpoint identity).
Result: each Server Action = one stable endpoint regardless of body; repeat calls = observed values; no fragmentation, no spurious failed. This is exactly parallel to the existing GraphQL / JSON-RPC handling.
Related
A separate, independent modern-format gap (TypeID/ULID path slotting) is filed separately.
Summary
Next.js Server Actions POST to the page URL and carry the operation in the
next-actionheader (the action id), with an RSC payload as the body (Content-Type: text/plain,Accept: text/x-component, plus anext-router-state-treeheader). The normalizer has no handling for this, so each action is treated as a generic POST and keyed by body content/shape — fragmenting one operation across multiple endpoint rows, and frequently failing normalization outright because the RSC body/headers break the pipeline's assumptions.Root cause — a missing case in an abstraction that already exists
src/session/normalize/protocol.tsalready models exactly this class. Its own comment:and exposes
OperationInfo { protocol, operation, opKeyHash }with values stripped. Butprotocolis typed"graphql" | "jsonrpc". Next.js Server Actions are the same class (header-dispatched;next-action= the operation id) but are not a recognized protocol, so they fall through to generic POST normalization. As a result they are never assigned aprotocol/operation/op_key_hash, and keying by body shape is both fragile (same action fragments when its body shape varies; different actions merge when shapes collide) and failure-prone.Fix
Add a
next-actionbranch to the protocol detector:next-actionrequest header (optionally corroborated byAccept: text/x-component).protocol: "nextjs-action",operation: <action-id>(a stable per-action hash),opKeyHash = sha16(canonicalPath + ":" + <action-id>).text/plainbody and thenext-router-state-treeheader from the key (they are transport/router state, not endpoint identity).Result: each Server Action = one stable endpoint regardless of body; repeat calls = observed values; no fragmentation, no spurious
failed. This is exactly parallel to the existing GraphQL / JSON-RPC handling.Related
A separate, independent modern-format gap (TypeID/ULID path slotting) is filed separately.