Skip to content

fix(slack): form encode Slack Web API requests - #3824

Closed
nahyeongjin1 wants to merge 1 commit into
Yeachan-Heo:devfrom
nahyeongjin1:fix/slack-web-api-form-encoding
Closed

fix(slack): form encode Slack Web API requests#3824
nahyeongjin1 wants to merge 1 commit into
Yeachan-Heo:devfrom
nahyeongjin1:fix/slack-web-api-form-encoding

Conversation

@nahyeongjin1

Copy link
Copy Markdown
Contributor

What changed

SlackLiveProvider#request now sends Slack Web API calls as
application/x-www-form-urlencoded instead of application/json.

Why

conversations.replies is form-encoded only. It answers invalid_arguments
for a JSON body:

POST conversations.replies  Content-Type: application/json
  -> ok=false  error=invalid_arguments
POST conversations.replies  Content-Type: application/x-www-form-urlencoded
  -> ok=true

findMessageByClientMsgId calls it for the reconciliation lookup that follows
an uncertain root post. #api turns the ok=false into a thrown
SlackProviderError, which is uncaught in the daemon's post path, so the
notification daemon dies before publishing ownership:

[Uncaught Exception] SlackProviderError: conversations.replies failed (web_api)
    at #api (slack-live-provider.ts:372)
    at async findMessageByClientMsgId (slack-live-provider.ts:269)
    at async findMessageByClientMsgId (slack-provider.ts:98)
    at async <anonymous> (slack-daemon.ts:1815)
    at async #postDurable (slack-daemon.ts:1814)
    at async notify (slack-daemon.ts:515)

Observed symptoms with valid credentials, scopes (chat:write,
channels:history), and a complete gjc notify setup slack:

  • gjc daemon restart slack -> slack daemon did not publish ownership after spawning
  • gjc notify test --provider slack -> slack runtime is not ready or attached
  • ensureSlackDaemon -> Unable to attach or spawn slack daemon owner
  • no session ever receives a Slack thread

gjc notify health --provider slack --probe reports OK throughout, because
auth.test and apps.connections.open both accept JSON. Only the read
methods reject it, which makes this hard to attribute to encoding.

Note conversations.history accepts JSON, so the failure only appears on the
thread-reply branch after the history lookup misses.

Why form encoding for all calls

Form encoding is Slack's canonical request format and is lossless here: every
#request body is a flat Record<string, string | undefined>. formBody
drops undefined entries so an absent optional field cannot reach Slack as the
literal string "undefined"JSON.stringify dropped them too, so this
preserves existing behavior.

Tests

The existing reconciliation test asserted only that the request body contained
client_msg_id, which held under both encodings — that is why this slipped
through. The added test asserts the wire encoding directly:

bun test packages/coding-agent/test/sdk-slack-live-provider.test.ts
  11 pass, 0 fail

Reverting the source change makes the new test fail with
Expected: application/x-www-form-urlencoded; charset=utf-8 /
Received: application/json; charset=utf-8.

Also run: biome check and tsc --noEmit clean on the changed files.

sdk-slack-daemon.test.ts and sdk-slack-thread-state.test.ts fail in a fresh
worktree because pi_natives.darwin-arm64.node is not built; they fail
identically on an unmodified checkout.

Verified against a live workspace

After patching a local 0.12.11 install, the daemon attached and posted real
session roots:

gjc daemon status slack  -> running
gjc notify test --provider slack -> OK - Slack notification test delivered

The provider sent every Slack Web API call with
`Content-Type: application/json`. `conversations.replies` is form-encoded
only and answers `invalid_arguments` for a JSON body, so the
reconciliation lookup that follows an uncertain root post threw an
uncaught SlackProviderError and killed the notification daemon before it
published ownership.

The failure surfaced as `slack runtime is not ready or attached` and
`Unable to attach or spawn slack daemon owner` with valid credentials,
scopes, and configuration, and no session ever received a Slack thread.

Form encoding is the canonical Slack request format and is lossless here:
every request body is a flat `Record<string, string | undefined>`.
Undefined entries are dropped so optional fields cannot reach Slack as
the literal string "undefined", which `JSON.stringify` also omitted.

The existing reconciliation test only asserted that the body contained
`client_msg_id`, which held under both encodings. The added test asserts
the wire encoding directly and fails against the previous behavior.

Verified with: bun test packages/coding-agent/test/sdk-slack-live-provider.test.ts

Co-authored-By: Claude <noreply@anthropic.com>
@nahyeongjin1

Copy link
Copy Markdown
Contributor Author

Duplicate of #3753, which landed the same fix two days earlier and has strictly stronger test coverage (exact ordered URLSearchParams equality and Headers normalization). Closing this in favor of #3753. Apologies for the noise — I did not check for an existing PR before opening.

@nahyeongjin1
nahyeongjin1 deleted the fix/slack-web-api-form-encoding branch August 4, 2026 14:21
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