Skip to content

fix(slack): typed ingress schemas + streaming/recipient/dedupe/upload correctness + tests - #365

Merged
rohitg00 merged 2 commits into
mainfrom
slack-fix-ingress-schemas
Jun 29, 2026
Merged

fix(slack): typed ingress schemas + streaming/recipient/dedupe/upload correctness + tests#365
rohitg00 merged 2 commits into
mainfrom
slack-fix-ingress-schemas

Conversation

@rohitg00

@rohitg00 rohitg00 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Post-merge fixes for the slack worker, from a full audit against the 0.20.0 workers (telegram-bot's streaming pattern, shell's schema pattern) and the open dep/convention PRs.

CI unblock

  • slack::events / slack::interactions are raw-Value HTTP handlers (must read the raw body off the request_body channel because Slack HMAC needs the exact bytes, unlike telegram-bot's header secret-token compare). A raw-Value handler publishes the AnyValue schema and failed interface-smoke --assert-typed-schemas. Fixed by attaching typed request/response schemas via RegisterFunction::request_format/response_format + a schema_value helper — the pattern shell and image-resize use.

Correctness (audit findings, verified against code)

  • Streaming race / double-start: serialize per session (RuntimeState::stream_lock) and gate on the message-updated revision (drop stale/out-of-order), mirroring telegram-bot's per-entry lock + revision monotonicity. Concurrent revision events no longer lose appended text or open two streams.
  • Channel streaming broken: send recipient_user_id and recipient_team_id for channels, neither for DMs (D…). chat.startStream into a channel was rejected before this.
  • Mid-stream rewrite: native append can't retract, so a non-prefix rewrite is skipped instead of duplicating content.
  • Unbounded dedupe: seen_events now time-windowed with an eviction cap.
  • files.upload: POST the bytes as multipart/form-data (-F file=@), not a raw body.
  • Identity on reload: keep last-known-good on a transient auth.test failure (so mention/self detection isn't blinded); slack::config-status still clears on an explicit check.
  • strip_mention: tidy the gap left by the removed mention; preserve other users' mentions and newlines.

Tests

+13 unit tests (per-session lock identity + serialization, revision guard, DM detection, delta reset/truncation, socket/http/bridge precedence, mention tidy/preserve, dedupe new-repeat + eviction). 34 unit + manifest + schema golden tests, all green; fmt + clippy -D warnings clean.

Skipped from the audit (with reason)

  • conversations.open selector validation — the whole API surface is thin passthrough by design; Slack validates.
  • self-loop hardening — already guarded by bot_id.is_some() || is_self.
  • pre-verify body-size cap — route is HMAC-gated; Slack payloads are small.

… handlers

interface-smoke --assert-typed-schemas flagged slack::events and
slack::interactions as AnyValue. They are raw-Value HTTP handlers that read the
request body off the request_body channel (Slack HMAC requires the exact raw
bytes, unlike telegram-bot's header secret-token compare), so the auto-derived
schema is empty.

Attach typed request/response schemas via RegisterFunction::request_format/
response_format with a schema-only request struct and a null response — the same
pattern shell and image-resize use for handlers whose auto-schema is wrong.

build, test, fmt, clippy -D warnings all green.
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jun 29, 2026 11:18am
workers-tech-spec Ready Ready Preview, Comment Jun 29, 2026 11:18am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a crate-internal schema_value<T>() helper in slack/src/functions/mod.rs that generates a JSON Schema, serializes it, and strips the "$schema" key. Two new typed structs (EventsHttpRequest, InteractionsHttpRequest) are introduced and wired into the slack::events and slack::interactions HTTP trigger registrations via this helper.

Slack HTTP Trigger Schema Wiring

Layer / File(s) Summary
schema_value helper
slack/src/functions/mod.rs
Adds pub(crate) fn schema_value<T: JsonSchema>() -> serde_json::Value that builds a JSON schema, serializes it, and removes the top-level "$schema" key.
Typed schema for slack::events
slack/src/functions/events.rs
Adds private EventsHttpRequest struct with body: Value and updates the register builder to use schema_value::<EventsHttpRequest>() as the request format.
Typed schema for slack::interactions
slack/src/functions/interactions.rs
Adds private InteractionsHttpRequest struct with body: Value and updates the register builder with explicit request_format and response_format (JSON null).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • iii-hq/workers#322: Introduces the same schema_value pattern (typed schemas, "$schema" key stripping) for other function registrations in the same codebase.

Poem

🐇 Hop hop, the schemas align,
No more untyped shapes at the shrine.
body: Value fields appear,
"$schema" stripped, the contract is clear.
This rabbit approves—everything's fine! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title reflects the main change to Slack typed ingress schemas, though it also mentions unrelated correctness/test items.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slack-fix-ingress-schemas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 28 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@slack/src/functions/events.rs`:
- Around line 23-24: The event payload schema is accidentally marking body as
optional because of the serde default on the Events function struct. Remove
#[serde(default)] from the body field in the relevant Events/event wrapper type
so schemars keeps body in required and the published trigger schema still
advertises the payload field downstream diagnostics expect.

In `@slack/src/functions/interactions.rs`:
- Around line 24-25: The published schema is incorrectly marking the
Interactions request `body` field as optional because `#[serde(default)]` is
present on the `body: Value` field in the `Interactions` request type. Remove
the defaulting behavior from that field so schemars keeps `body` required, and
verify the request struct that defines `body` still serializes/deserializes
correctly without implying a missing payload is allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 463f5380-1826-49de-bcec-02ad9cf31d1e

📥 Commits

Reviewing files that changed from the base of the PR and between 5c62ed9 and 2728711.

📒 Files selected for processing (3)
  • slack/src/functions/events.rs
  • slack/src/functions/interactions.rs
  • slack/src/functions/mod.rs

Comment on lines +23 to +24
#[serde(default)]
body: Value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In schemars 0.8, does marking a struct field with #[serde(default)] remove that field from the generated JSON Schema "required" array?

💡 Result:

Yes, in schemars 0.8, marking a struct field with #[serde(default)] (or the equivalent #[schemars(default)]) will remove that field from the generated JSON Schema's "required" array [1][2][3]. By applying this attribute, you are informing schemars that the field has a default value, which allows the generated schema to treat the field as optional. Additionally, schemars will generally include the default value in the "default" property of the field's schema definition, unless that behavior is suppressed by other attributes like skip_serializing_if [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' slack/src/functions/events.rs
printf '\n---\n'
sed -n '1,220p' lsp/src/diagnostics.rs

Repository: iii-hq/workers

Length of output: 9915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the schema helper and the registration path.
rg -n "schema_value::<|fn schema_value|request_format\\(" slack src lsp -g '!target'

Repository: iii-hq/workers

Length of output: 507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' slack/src/functions/mod.rs
printf '\n---\n'
sed -n '1,120p' slack/src/functions/interactions.rs

Repository: iii-hq/workers

Length of output: 7671


Keep body required in the published schema Remove #[serde(default)]; schemars will drop body from required, so the trigger schema no longer advertises the payload field downstream diagnostics expect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slack/src/functions/events.rs` around lines 23 - 24, The event payload schema
is accidentally marking body as optional because of the serde default on the
Events function struct. Remove #[serde(default)] from the body field in the
relevant Events/event wrapper type so schemars keeps body in required and the
published trigger schema still advertises the payload field downstream
diagnostics expect.

Comment on lines +24 to +25
#[serde(default)]
body: Value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In schemars 0.8, does marking a struct field with #[serde(default)] remove that field from the generated JSON Schema "required" array?

💡 Result:

Yes, in schemars 0.8, marking a struct field with #[serde(default)] (or #[schemars(default)]) causes the field to be excluded from the generated JSON Schema "required" array [1][2][3]. When this attribute is present, the field is treated as optional in the generated schema, and the field's schema object will include a "default" property, unless it is explicitly skipped by a #[serde(skip_serializing_if = "...")] attribute [1][2][3]. This behavior aligns with the library's goal of ensuring the generated schema reflects how serde_json serializes and deserializes the corresponding Rust struct [4][5]. While there have been minor regressions and fixes regarding this logic in various 0.8.x patch releases (such as handling conflicts between default, required, and skip_serializing_if), the standard functionality is that #[serde(default)] signals to the schema generator that the field is not required [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the request struct and schema wiring in the target file.
sed -n '1,120p' slack/src/functions/interactions.rs

# Look for the request schema generation path used by this codebase.
rg -n "schema_value::<InteractionsHttpRequest>|request_format\\(|required\\]" slack/src lsp/src

Repository: iii-hq/workers

Length of output: 3656


Keep body required in the published schema

#[serde(default)] makes schemars treat body as optional, so downstream tooling will not flag a missing payload even though this request shape depends on it.

Possible fix
 struct InteractionsHttpRequest {
     /// The url-encoded interactivity body (also delivered raw on the request_body channel).
-    #[serde(default)]
     body: Value,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[serde(default)]
body: Value,
body: Value,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slack/src/functions/interactions.rs` around lines 24 - 25, The published
schema is incorrectly marking the Interactions request `body` field as optional
because `#[serde(default)]` is present on the `body: Value` field in the
`Interactions` request type. Remove the defaulting behavior from that field so
schemars keeps `body` required, and verify the request struct that defines
`body` still serializes/deserializes correctly without implying a missing
payload is allowed.

…+ tests

Audit against the 0.20.0 workers (telegram-bot streaming pattern, shell schema
pattern) surfaced real defects beyond the typed-schema CI fix:

- streaming: serialize per-session (RuntimeState::stream_lock) and gate on
  message-updated revision (drop stale/out-of-order), mirroring telegram-bot's
  per-entry lock + revision monotonicity. Fixes lost-update races and double
  chat.startStream under concurrent revision events.
- channel streaming: send recipient_user_id AND recipient_team_id for channels,
  none for DMs (channel id starts with 'D'). Native channel streaming was
  rejected by Slack before this.
- delta: skip a non-prefix rewrite on the native append path (can't retract),
  instead of duplicating content.
- dedupe: bound seen_events with a time window + eviction cap (was unbounded).
- files.upload: POST the bytes as multipart/form-data (Slack's -F file=@ shape),
  not a raw body.
- identity: keep last-known-good on a transient reload auth.test failure
  (config-status still clears on an explicit check).
- strip_mention: tidy the gap left by a removed mention; keep other users'
  mentions and newlines.

Tests: +13 (per-session lock identity/serialization, revision guard, DM
detection, delta reset/truncation, socket/http/bridge precedence, mention
tidy/preserve, dedupe new/repeat + eviction). 34 unit + manifest + schema, all
green; fmt + clippy -D warnings clean.

Skipped from the audit (with reason): conversations.open selector validation
(thin passthrough by design), self-loop hardening (already guarded by
bot_id.is_some() || is_self), pre-verify body size cap (HMAC-gated route).
@rohitg00 rohitg00 changed the title fix(slack): publish typed schemas for events/interactions ingress fix(slack): typed ingress schemas + streaming/recipient/dedupe/upload correctness + tests Jun 29, 2026
@rohitg00
rohitg00 merged commit a4ee960 into main Jun 29, 2026
20 of 21 checks passed
@rohitg00
rohitg00 deleted the slack-fix-ingress-schemas branch June 29, 2026 11:22
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