Skip to content

fix: stop stripping header level from sent Block Kit payloads - #180

Merged
StephenTangCook merged 1 commit into
mainfrom
claude/header-level-stripping-bug-7dtpk8
Aug 14, 2026
Merged

fix: stop stripping header level from sent Block Kit payloads#180
StephenTangCook merged 1 commit into
mainfrom
claude/header-level-stripping-bug-7dtpk8

Conversation

@StephenTangCook

@StephenTangCook StephenTangCook commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

toSlackBlocks deleted the level field from every header block on its way out, so the builder silently dropped the heading level from every message it sent. level is a real Slack field, not a builder-only extension — this removes the carve-out and corrects the docs, type comments, editor help text, and README that repeated the wrong assumption.

Why

The strip happened in src/lib/to-slack-blocks.ts, one step before the payload leaves the builder:

if (safe.type === 'header' && 'level' in safe) {
  const { level: _omit, ...rest } = safe;   // ← here
  return rest as SupportedBlock;
}

The JSDoc explained it as "the cosmetic level field from header blocks (a builder-only extension Slack would reject)", and HeaderEditor repeated it in the UI: "Optional. Builder-only extension; Slack's API ignores this value."

That assumption is wrong. Slack's header block accepts an optional level integer 1–4. This package's own validator dependency agrees — @tightknitai/slack-block-kit-validator@0.1.13 lists level among the header block's allowed keys under additionalProperties: false:

"level": { "type": "integer", "minimum": 1, "maximum": 4 }

Under additionalProperties: false an unknown property would be rejected outright, so the schema treats level as legitimate. Confirmed against the resolved dependency at runtime — levels 1–4 validate, 0 and 5 produce below minimum 1 / above maximum 4.

The bug was invisible right up to the send, which is what made it hard to spot: the preview renders from the working draft (via sanitizeBlock, not toSlackBlocks), and the JSON drawer stringifies the working draft directly — both still showed level. Only the payload handed to the send/update callbacks had it removed.

sanitizeBlock is kept as-is; the URL scrubbing and retrieval-only-key stripping are unrelated and still needed.

One behavior change worth noting

Validation runs on toSlackBlocks(...) output, so level now reaches the validator for the first time. Values 1–4 pass and the send CTA stays enabled. An out-of-range value that previously vanished silently now surfaces as a validation error (blocks[0].level: above maximum 4) — the intended outcome, but it means a draft carrying e.g. level: 6 will report an error where it previously did not. The editor only ever offers 1–4, so this affects payloads supplied programmatically or loaded from URL state.

Test plan

  • pnpm typecheck
  • pnpm lint
  • pnpm test — 38 files, 452 tests passing (both the unit and storybook browser projects)
  • Storybook interaction coverage added rather than spot-checked by hand

Test changes:

  • test/public-api.test.ts — the existing case asserted the buggy behavior (strips the builder-only 'level' field), so it is inverted to assert preservation. Added cases covering the full 1–4 range against the real validator, the out-of-range error path, and that a header with no level is returned reference-unchanged.
  • src/components/editors/block-editor.stories.tsx — new SelectingHeaderLevelProducesValidBlock interaction story clicking the H2 radio and asserting the level survives toSlackBlocks into a valid payload, covering the editor → wire path end to end.

Notes for reviewer

Scoped to this package. The originating report also asked to bump @tightknitai/block-kitchen in apps/web-studio-start and update a now-stale comment at send-message-dialog.tsx:272-274 — that lives in another repo and needs this released first, so it is not included here. The stopgap suggested there (re-attaching level after toSlackBlocks in the consumer) should not be needed once this ships, and would leave the package's built-in SendDialog still dropping the field.

One environment note in case CI or a local run trips on it: the storybook browser project wants a Playwright build (chromium_headless_shell-1234) that this sandbox did not have, and cdn.playwright.dev is blocked here. I ran that project against the locally installed Chromium to verify it — all 118 of its tests pass, including the new story. Unrelated to this diff, but it is why a fresh environment may need pnpm exec playwright install first.


Generated by Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

`toSlackBlocks` deleted the `level` field from every header block on the
way out, on the assumption that it was a builder-only extension Slack
would reject. That assumption was wrong: `level` is a real Slack field on
the header block — an optional integer 1-4 — so the builder silently
dropped the heading level from every message it sent.

The bug was invisible right up to the send. The preview and the JSON
drawer both read the working draft, which still carries `level`; only the
payload handed to `chat.postMessage` / `chat.update` had it removed.

Confirmation that `level` is legitimate: `@tightknitai/slack-block-kit-validator`
lists it among the header block's allowed keys under
`additionalProperties: false` and validates it as an integer with
minimum 1 / maximum 4. An unknown property would be rejected outright.

Drop the header carve-out and keep `sanitizeBlock` — the URL scrubbing
and retrieval-only-key stripping are unrelated and still needed. Since
validation runs on `toSlackBlocks(...)` output, `level` now reaches the
validator for the first time: values 1-4 pass, and out-of-range values
correctly surface as validation errors instead of being discarded.

Also corrects the JSDoc, the `HeaderLevel` type docs, the editor help
text, and the README, all of which repeated the same wrong assumption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ao4W1Fn3JoatgX3RS6h68
@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
block-kitchen cfba02f Commit Preview URL

Branch Preview URL
Aug 14 2026, 06:06 AM

@StephenTangCook
StephenTangCook merged commit e8724bf into main Aug 14, 2026
14 checks passed
@StephenTangCook
StephenTangCook deleted the claude/header-level-stripping-bug-7dtpk8 branch August 14, 2026 12:05
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.

2 participants