Skip to content

publish.headers is declared in the SDK types and documented as a filter, but nothing dispatches it #393

Description

@asachs01

publish.headers is declared as a core filter next to publish.html (src/core/plugin-sdk/types/hooks.ts:52-64, context type at :80):

export interface CmsServerFilters {
  'publish.html': string
  'publish.headers': Record<string, string>

and documented as one (docs/features/plugin-system.md:544):

Filters: publish.html, publish.headers, content.entry.cells.

Nothing dispatches it. Every occurrence in the repo is a declaration, a comment, or that doc line:

file:line what it is
src/core/plugin-sdk/types/hooks.ts:54 filter value type
src/core/plugin-sdk/types/hooks.ts:80 filter context type
src/core/plugins/hookBus.ts:162 JSDoc example on applyFilter
server/plugins/protocol/messages.ts:164 comment
docs/features/plugin-system.md:544 docs

The three production applyFilter calls are publish.html (server/publish/publishedHtmlPipeline.ts:61), content.entry.cells (server/publish/contentEvents.ts:79) and media.url.transform (server/publish/mediaPresentation.ts:60). Published-page responses assemble their headers directly (server/publish/publicRouter.ts:237,250,273,316,324,337,340) and never reach the bus. publishedHtmlPipeline.ts:9-23 documents its own stages and does not list it either.

Reproduce: register a handler and publish.

hooks.filter('publish.headers', (headers) => ({ ...headers, 'x-plugin': '1' }))

No error, no warning, no header. It fails silently in both directions: handleHooksFilter registers any name without validation (server/plugins/host/handlers/hooks.ts:32-43), and applyFilter early-returns when a name has no handlers (hookBus.ts:169-170).

Structurally this is a typing asymmetry rather than an oversight in one file. emit() is typed against the closed CoreHookEvent union (hookBus.ts:36-42), so a core event cannot be declared without something wiring it. applyFilter(name: string, …) (hookBus.ts:164-168) takes a raw string, so a filter can sit in the SDK types indefinitely with nothing on the host side to match it.

Either direction resolves it:

  • dispatch it in publicRouter.ts where the published response headers are assembled, passing the { siteId, pageId, slug } context the type already declares; or
  • delete the two type members and the doc line, since the pre-release rule is no shims for surfaces that do not exist.

Happy to send a PR for whichever you prefer — I did not want to guess which, since implementing it makes a new public surface real.

Verified at 6b055cf7 (v0.0.16-3).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions