Skip to content

HttpApi: per-slot ParseOptions annotations with ParseOptions fallback - #8572

Merged
tim-smart merged 9 commits into
mainfrom
agent/nelson/446844de5a11
Sep 27, 2026
Merged

tim-smart merged 9 commits into
mainfrom
agent/nelson/446844de5a11

Conversation

@tim-smart

@tim-smart tim-smart commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Setting HttpApi.ParseOptions to { onExcessProperty: "error" } breaks every endpoint with declared headers and every WithHeaders response. That's because header maps always carry transport headers like content-type and user-agent. #8557 fixed this by quietly clearing onExcessProperty for header codecs. This PR instead lets users set parse options per codec slot.

Changes

  • Adds six annotations to HttpApi: ParamsParseOptions, QueryParseOptions, HeadersParseOptions, PayloadParseOptions, SuccessParseOptions and ErrorParseOptions.
  • Each slot uses its own annotation if it is set on the API, group or endpoint, then falls back to ParseOptions, then to Schema defaults. A slot annotation at any level beats ParseOptions at any level. It replaces ParseOptions and is never merged with it. Headers follow the same rule as every other slot.
  • HttpApiBuilder, HttpApiClient and urlBuilder resolve options per slot. WithHeaders response headers (success and error, buffered and streamed) use HeadersParseOptions.
  • The buffered client WithHeaders path no longer decodes a single Struct({ body, headers }) union with one set of options. Each response schema now decodes its body with the success/error options and its headers with the headers options, the same way the streamed path already works. There is no wrapper schema and no as any.
  • The ParseOptions JSDoc lists the slot annotations and the resolution rule. It also explains the transport-header problem ("error" rejects real traffic, "preserve" leaks transport headers into decoded values) and shows the fix:
HttpApi.make("Api")
  .add(...)
  .annotate(HttpApi.ParseOptions, { onExcessProperty: "error" })
  .annotate(HttpApi.HeadersParseOptions, {})

Notes

  • encodeToWithHeaders schemas are handled like structural WithHeaders: the annotation stores the user mapping plus the body and header codecs, so the server and client encode/decode the body with the success/error options and the headers with the headers options, then apply the mapping. HttpApiEndpoint no longer wraps these schemas in a { body, headers } struct; it only swaps in the response codecs.
  • On the server, structural WithHeaders errors and encodeToWithHeaders responses share one response schema builder. On the client, each buffered WithHeaders schema becomes a decode-only schema that applies the slot options internally, so response unions stay plain Schema.Unions.

Validation

  • pnpm vitest --run packages/effect/test/httpapi/ packages/effect/test/reactivity/AtomHttpApi.test.ts packages/effect/test/http plus the platform-node HttpApi tests: 668 tests pass. This includes the tests-first commit and the existing Expose HttpApi.ParseOptions for schema decoding and encoding #8269 ParseOptions tests.
  • pnpm check, pnpm lint, pnpm jsdocs --check and pnpm doctest --run packages/effect/src/http-api/HttpApi.ts all pass.

Closes EFF-1607
Closes #8557

Tests-first for EFF-1607: covers ParamsParseOptions, QueryParseOptions,
HeadersParseOptions, PayloadParseOptions, SuccessParseOptions and
ErrorParseOptions on the server (HttpApiBuilder) and client
(HttpApiClient, urlBuilder), including fallback to ParseOptions,
API-level slot annotations beating endpoint ParseOptions, replace-not-merge
semantics, and strict ParseOptions with HeadersParseOptions {} accepting
transport headers on requests and buffered/streamed WithHeaders responses.
Adds ParamsParseOptions, QueryParseOptions, HeadersParseOptions,
PayloadParseOptions, SuccessParseOptions and ErrorParseOptions. Each slot
uses its own annotation when set on the API, group or endpoint, otherwise
falls back to ParseOptions. The buffered client WithHeaders path now
decodes headers and bodies with their own options.
@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 21ba2e5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 31 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 38.54 KB 38.54 KB 0.00 KB (0.00%)
basic.ts 6.88 KB 6.88 KB 0.00 KB (0.00%)
batching.ts 9.97 KB 9.97 KB 0.00 KB (0.00%)
brand.ts 6.57 KB 6.57 KB 0.00 KB (0.00%)
cache.ts 10.73 KB 10.73 KB 0.00 KB (0.00%)
config.ts 21.81 KB 21.81 KB 0.00 KB (0.00%)
differ.ts 20.95 KB 20.95 KB 0.00 KB (0.00%)
http-client.ts 22.11 KB 22.11 KB 0.00 KB (0.00%)
http-router.ts 33.33 KB 33.33 KB 0.00 KB (0.00%)
logger.ts 10.85 KB 10.85 KB 0.00 KB (0.00%)
metric.ts 8.83 KB 8.83 KB 0.00 KB (0.00%)
optic.ts 6.80 KB 6.80 KB 0.00 KB (0.00%)
pubsub.ts 15.00 KB 15.00 KB 0.00 KB (0.00%)
queue.ts 11.88 KB 11.88 KB 0.00 KB (0.00%)
schedule.ts 11.03 KB 11.03 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.48 KB 13.48 KB 0.00 KB (0.00%)
schema-binary.ts 39.53 KB 39.53 KB 0.00 KB (0.00%)
schema-class.ts 20.65 KB 20.65 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 31.72 KB 31.72 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 26.92 KB 26.92 KB 0.00 KB (0.00%)
schema-string-transformation.ts 14.18 KB 14.18 KB 0.00 KB (0.00%)
schema-string.ts 11.73 KB 11.73 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.75 KB 15.75 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.08 KB 38.08 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 25.13 KB 25.13 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 19.88 KB 19.88 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 20.04 KB 20.04 KB 0.00 KB (0.00%)
schema-toFormatter.ts 20.14 KB 20.14 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 24.64 KB 24.64 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 20.14 KB 20.14 KB 0.00 KB (0.00%)
schema.ts 19.86 KB 19.86 KB 0.00 KB (0.00%)
stm.ts 12.90 KB 12.90 KB 0.00 KB (0.00%)
stream.ts 9.82 KB 9.82 KB 0.00 KB (0.00%)

Replace the per-slot cases with one server scenario and three client
scenarios that relax one slot at a time, covering fallback, API-level
slots over endpoint ParseOptions, replace-not-merge, and the separate
header/body decoding for buffered and streamed WithHeaders responses.
Server: encodeToWithHeaders response headers must use HeadersParseOptions
rather than the error body options, in both directions.
Client: header transformations must decode with HeadersParseOptions.
Store the user mapping and body/header wire codecs in the
encodeToWithHeaders annotation, so the server and client handle these
schemas like structural WithHeaders: the body uses the success/error
options, the headers use the headers options, then the mapping applies.
This removes the client's two-pass header check and the double header
validation.
@tim-smart
tim-smart merged commit fb718d3 into main Sep 27, 2026
13 checks passed
@tim-smart
tim-smart deleted the agent/nelson/446844de5a11 branch September 27, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant