Skip to content

fix(client): send a JSON body on every body-less mutating request - #106

Merged
justinhelmer merged 1 commit into
mainfrom
fix/json-body-on-mutations
Sep 18, 2026
Merged

justinhelmer merged 1 commit into
mainfrom
fix/json-body-on-mutations

Conversation

@justinhelmer

Copy link
Copy Markdown
Contributor

polylane cloud disconnect failed with "Invalid JSON response from server (status 403)" because it sent a DELETE with no body. The shared HTTP client now sends an empty JSON object on every body-less POST, PUT, PATCH and DELETE, so any command can reach the API.

Why: The Polylane API edge rejects a mutating request without a JSON content type with a 403 HTML page before a worker sees it (nominal#1575). #83 patched one call site; #105 is the next one, found while collecting Trigger.dev receipts on prod.

Where to look

  1. The client picks the payload once: the caller's body, nothing for GET, an empty object for any other method. ⚠ A caller that relied on a truly body-less POST would now send {}; none does.
  2. Its test pins all four mutating methods, a caller-supplied body and the GET exception; the four method cases fail on main.
  3. bind-run drops the per-call body: {} workaround from fix(auth): send a JSON body on the bind-run POST so the API edge lets it through #83; its existing test still asserts the {} body, now supplied by the client.

Feedback wanted: Whether to special-case any method besides GET. HEAD and OPTIONS are not in HttpMethod, so the rule reads as "GET is the only body-less method".

Risk: Every mutating CLI request now carries a content type and a two-byte body; the API ignores an empty object on routes without input. Rollback is a revert.

Verified: cli suite 540 pass; the new test's four mutating-method cases fail with the fix reverted; eslint and tsc clean.

Validation (4 criteria)
Criterion Proof
POST, PUT, PATCH and DELETE without a body send content-type: application/json and {} test/client-http-body.test.ts (4 cases), 6 pass at 52f8cde; 4 fail with the payload rule reverted
A caller-supplied body is sent unchanged test/client-http-body.test.ts::a body given by the caller is sent as is
GET stays body-less test/client-http-body.test.ts::GET stays body-less with no content type
auth bind-run still sends {} without its own workaround test/auth-bind-run.test.ts::sends a JSON content type and an empty object body so the API edge lets it through
For agents

Prod repro on 0.2.38: polylane cloud disconnect acc_… --yes --output json{"error":{"code":1,"message":"Invalid JSON response from server (status 403)"}}; the same DELETE with the JSON header and {} returns 200 (2026-09-18T00:03:11Z). The generated API client is gitignored (npm run codegen regenerates src/client/client.ts), every generated operation goes through request() in src/client/http.ts.

🤖 Generated with Claude Code

…the API edge lets it through

The API edge answers a POST, PUT, PATCH or DELETE without a JSON content type
with a 403 HTML page before it reaches a worker (nominal#1575). `cloud
disconnect` sent a body-less DELETE and failed with "Invalid JSON response from
server (status 403)" (#105). The client now sends an empty object on any
mutating request without a body of its own, which also retires the per-call
workaround in `auth bind-run`.

Closes #105

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@coreplane-switchboard coreplane-switchboard Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM: Correct, minimal fix at the shared client with method-by-method tests; workaround removal in bind-run stays covered.

Approve — the fix is correct, minimal, and well tested. Reviewed head 52f8cde.

What it does: src/client/http.ts now resolves the payload once — caller's body, else undefined for GET, else {} for POST/PUT/PATCH/DELETE — so every body-less mutating request carries Content-Type: application/json and {}, which the API edge requires (nominal#1575). bind-run.ts drops its per-call body: {} workaround, now redundant.

Checks I ran:

  • The payload rule uses the resolved method (opts.method ?? 'GET'), so a default-GET request correctly stays body-less.
  • HttpMethod is exactly GET|POST|PUT|PATCH|DELETE, so the "GET is the only body-less method" rule is exhaustive — no HEAD/OPTIONS to worry about.
  • The dry-run and query/header paths are untouched; the only behavioral delta is the two-byte body plus header on previously body-less mutations, which the PR body correctly flags as ignored by the API.
  • New test test/client-http-body.test.ts pins all four mutating methods, caller-supplied body pass-through, and the GET exception via a fetch stub with clean restore; bind-run's existing test still asserts the {} body per the PR. No tests removed or weakened; no docs/reference/specs/ in this repo, so no spec check applies.

On the "feedback wanted" question: agreed with the current shape — with HEAD/OPTIONS absent from the type, no further special-casing is warranted; if they're ever added, they should join GET in the body-less branch.

No findings.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: coreplane-switchboard[bot] reviewed this PR and posted an LGTM verdict (see its review). A repo admin enabled this via the auto-approve workflow.

@justinhelmer
justinhelmer merged commit f51e1b2 into main Sep 18, 2026
4 checks passed
@justinhelmer
justinhelmer deleted the fix/json-body-on-mutations branch September 18, 2026 08:32
@justinhelmer

Copy link
Copy Markdown
Contributor Author

Prod receipt on 0.2.39 (npm latest = 0.2.39, release run https://github.com/coreplanelabs/cli/actions/runs/35324998879): polylane cloud disconnect acc_… --yes --output json at 2026-09-18T08:38:36Z returned the account object (200) instead of the 0.2.38 Invalid JSON response from server (status 403). The same account reconnected 3.7 s later with cloud connect (also on 0.2.39), no failures. Closes the loop on #105.

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