Skip to content

fix(connector-openapi): send each parameter by its declared style and explode - #28537

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-28504-openapi-param-style
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-28504-openapi-param-style

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #28504
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:OpenAPI 连接器发请求时,query / path / header 参数按文档声明的 style / explode(或 OpenAPI 3.x 默认值)编码,对象和数组不再经 String() 变成 [object Object] 或逗号串。
  • 为什么改:Stripe 等厂商的列表接口用 deepObject 传日期区间和 expand,此前请求被静默发错、运行却答 200;作者和 AI 按 schema 填参也照样发错。
  • 风险与回滚:标量参数逐字节不变(对照用例在旧代码上同样通过);新增两处拒绝——文档给某位置声明了未定义的 style 时启动失败(仓内 2 份文档 42 个参数实测 0 命中),值的形状无编码时该步失败且不发请求。回滚即还原本 PR。
  • 先看:packages/connectors/connector-openapi/src/parameter-serialization.ts(serializeQueryParameter 与 deepPairs,deepObject 数组取下标括号);openapi-connector.ts 的 parameterEncodings / withAuthQuery(启动期拒绝与 api-key 合并);parameter-style.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/connector-openapi exec vitest run --maxWorkers=2 src/parameter-style.test.ts,预期 17 passed,其中 Stripe 的 created 发成 created%5Bgte%5D=1700000000。
  • 需要你做的:无,席位落地。

What changes

  • New src/parameter-serialization.ts: each sent parameter is encoded by its Parameter Object's style / explode, or by its location's OpenAPI 3.x default when it declares none (query form, exploded; path and header simple). Implemented: query form, spaceDelimited, pipeDelimited, deepObject; path simple, label, matrix; header simple. Scalars are the only values that become text.
  • deepObject on an array (Stripe's expand) brackets each index, expand[0]=a&expand[1]=b: the style's own rule (each property in brackets) applied to the array's keys, rather than a guessed expand[] convention. Nested values recurse the same way.
  • Materialization refusal: a parameter whose style OpenAPI 3.x does not define for its location, or a non-boolean explode, throws from createOpenApiConnector naming the operation, the parameter and the style (fatal at boot for a declarative instance; include that leaves the operation out clears it).
  • Dispatch refusal (ParameterValueError): a value the declared style has no encoding for (an array of objects for form, an object for exploded spaceDelimited, any non-scalar for a content-typed parameter) fails the step before fetch is called.
  • Query pairs travel as an ordered list; the static api-key query credential still replaces a same-named input pair in place, as before.
  • OpenApiParameter gains optional style, explode, content.
  • Docs: content/docs/automation/connectors.mdx states how parameters are sent and adds the two refusals to the failure table; packages/apps/connector-stripe/README.md Limits bullet, which told authors to leave expand / created[gte] out, now says how they are sent (this change made it false).
  • Changeset: @objectstack/connector-openapi patch.

Tests

  • src/parameter-style.test.ts (17): the three Stripe shapes with GetInvoices' five parameter objects copied verbatim from Stripe's document (MIT), equal to the packaged connector-stripe document's objects (one-off canonical-JSON compare: equal true); two scalar controls (query and path); the style table; the auth-merge case; both refusals, the dispatch refusal asserting the error class and zero fetch calls.
  • Ablation (one-off, base openapi-connector.ts swapped in, restored with git checkout HEAD --, hash re-matched): 15 failed / 2 passed of 17. The 2 that stay green are the scalar controls, so scalar output is byte-identical to the old code. Old outputs read off the failures: created=%5Bobject+Object%5D, expand=data.customer%2Cdata.subscription, due_date=%5Bobject+Object%5D.
  • Public door, one-off probe (not committed): the packaged Stripe app booted with bootStack, a flow calling GetInvoices via POST /api/v1/automation/:flow/trigger answered 200 and the fake Stripe received https://api.stripe.com/v1/invoices?created%5Bgte%5D=1700000000&due_date%5Bgte%5D=1700000000&due_date%5Blt%5D=1702592000&expand%5B0%5D=data.customer&expand%5B1%5D=data.subscription&limit=3.

Acceptance notes

  • Stripe's own answer to expand[0]=… and created[gte]=… is NOT MEASURED (no key); the tests run against a fake Stripe.
  • allowReserved is not read: reserved characters stay percent-encoded, as before.
  • Cookie parameters are still left out of the action's input schema and never sent (unchanged).
  • Query delimiters are form-urlencoded with the values (, as %2C, a space as +, | as %7C), which is how a scalar was always encoded; path delimiters stay literal.
  • An empty array or object sends nothing (RFC 6570 treats it as undefined).

Verification

All readings at head 73e2a2ec (after merging origin/main at eeec88d1), repository objectstack-ai/objectstack.

Package tests and typecheck
  • pnpm --filter @objectstack/connector-openapi test: Test Files 9 passed (9), Tests 109 passed (109).
  • pnpm --filter @objectstack/connector-openapi typecheck: exit 0; check:test-typecheck: OK, 1 file / 5 errors / 1 pinned signature held (pre-existing ledger, unchanged).
  • pnpm --filter @objectstack/connector-app-stripe test (the consumer that boots the packaged Stripe document): objectstack validate passed, Test Files 2 passed (2), Tests 16 passed (16).
  • Lock verdict: os-verify-lock: VERDICT command-exit 0.
Derived gate families
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 88 commands for the 6 changed paths; every one run on 73e2a2ec, each exit code captured before any pipe: 88 exit 0.
  • --ran reconciliation: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN.
  • node scripts/check-nul-bytes.mjs: exit 0.
  • Not run locally, owned by CI: the type-check lanes, Build Core, Build Docs and the families the derivation prints as outside its list; the full pnpm lint sweep.

Generated by Claude Code

…and explode

Query, path and header parameter values were passed through String(), so an
object went out as [object Object] and an array as a comma list whatever the
document declared. Each value is now encoded by its Parameter Object's style /
explode (or the OpenAPI 3.x default for its location); an undefined style
refuses the operation at materialization, and a value the style cannot carry
fails the dispatch before anything is sent.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
…e Stripe fixtures

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
The Limits bullet told authors to leave expand and the created range filters
out because they went out as plain pairs; they are now sent by their declared
deepObject style.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/connector-app-stripe, @objectstack/connector-openapi, touching 32 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/apps/connector-stripe/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via deepObject (literal, a string literal in query; a string literal in serializeQueryParameter))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/apps/connector-stripe/README.md) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 37184244b82b9e4234581b653cdc7554e74e6136 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f63a5da1c63f8fa68818014a1ce4598b201adea3 — the merge of head 73e2a2ec62142c8ad97cbec4619beb9ff9adc241 into base 37184244b82b9e4234581b653cdc7554e74e6136, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f63a5da1c63f8fa68818014a1ce4598b201adea3 && git checkout f63a5da1c63f8fa68818014a1ce4598b201adea3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 37184244b82b9e4234581b653cdc7554e74e6136 73e2a2ec62142c8ad97cbec4619beb9ff9adc241 && git checkout -B drift-repro 37184244b82b9e4234581b653cdc7554e74e6136 && git merge --no-ff 73e2a2ec62142c8ad97cbec4619beb9ff9adc241

node scripts/docs-audit/affected-docs.mjs --json 37184244b82b9e4234581b653cdc7554e74e6136

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 37184244b82b9e4234581b653cdc7554e74e6136 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

席位核对

ACCEPT at head 73e2a2ec · Implemented-by: claude/issue-28504-openapi-param-style · Reviewed-by: this seat (lane ACCEPT at TIER_DEFAULT). Clause-②: no. No packages/spec file is touched, so no CONTRACT_REVIEW_TIER record is owed.

Check Result
Shape Draft, base main. First line is Fixes #28504, the only closing keyword.
Scope 6 files, +696/−29. Not governed (0 of 6 paths hit the register). One @objectstack/connector-openapi patch changeset.
Ruling Triage 6553109544 is followed. Every path, query and header value is encoded by its declared style / explode, or by the OAS 3.x default for its location. A style that OAS 3.x does not define for the location refuses the operation at materialization, naming the operation and the parameter; an include that leaves the operation out clears the refusal. A value the style cannot carry fails the dispatch before any fetch. No non-scalar value reaches String().
Evidence 17 cases, including the three Stripe shapes with GetInvoices' five parameter objects copied verbatim; their canonical JSON equals the packaged stripe.openapi.json. The ablation turns 15 red and leaves exactly the 2 scalar controls green, which confirms scalars are byte-identical to before. A public-door probe through POST /automation/:flow/trigger sent created%5Bgte%5D=…&due_date%5Bgte%5D=…&expand%5B0%5D=data.customer&expand%5B1%5D=data.subscription. Stripe's own answer is NOT MEASURED (no key).
Gates Local: 88 derived, 88 run, 0 unrun. CI on the head is still running: 14 success, 12 skipped, 21 in progress, none red so far.
Accepted deviation packages/apps/connector-stripe/README.md is outside the claimed surface. Its Limits bullet told authors to leave expand / created out, and this change makes that false. Correcting it in the same PR is right.
Open question (dev: A, shipped) deepObject on an array is undefined in OAS 3.x. Stripe declares it on expand. The seat keeps A (expand[0]=a), because it follows from the declared style's own rule and is also the indexed form Stripe's form decoder reads. Whether Stripe accepts it is NOT MEASURED. If a measurement with a key ever disagrees, a new card corrects it.

Noted, not filed (agreed): cookie parameters are never sent, and allowReserved is not read. Both predate this PR, and neither has a public-door reach measured.

Landing: not governed. The seat lands it (pr_ready + automerge_enable) once CI on this head is green.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 05:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 05:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit a52191f Nov 1, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-28504-openapi-param-style branch November 1, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant