Skip to content

Endpoint normalization: model Next.js Server Actions (next-action header) as first-class operations #98

Description

@Serhatcck

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions