Skip to content

Handler returning a response status not declared in the spec (with a map/array body) fails with SENR0001 instead of JSON #127

Description

@joewiz

[This issue was co-authored with Claude Code. -Joe]

Summary

When a route handler responds with a status code that is not declared in the operation's responses (and the handler does not pass an explicit media-type), roaster falls back to application/xml and sets output:method=xml. If the body is a map(*) (the normal shape for a JSON API), serialization then fails with:

SENR0001 Cannot serialize a map(*) with the XML or text output method

So instead of a usable JSON response, the caller gets an opaque serialization error. This is most painful for error paths: any unexpected/edge error tends to map to a status (commonly 500) that the spec doesn't enumerate, and you can't realistically declare every possible status on every route.

Root cause (roaster 1.12.1)

In router.xql:

  • router:write-response resolves the content-type as head(($response?RESPONSE_TYPE, router:get-content-type-for-code($config, $code, "application/xml"))) — i.e. when the handler didn't set an explicit media-type, it uses get-content-type-for-code with an application/xml fallback.
  • router:get-content-type-for-code looks up responses[string($code)] (or responses.default); if the status isn't declared and there's no default, it returns the application/xml fallback.
  • router:write-response then sets Content-Type: application/xml and util:declare-option("output:method", "xml"). A map(*)/array(*) body cannot be serialized with the XML method → SENR0001.

Minimal repro

A route that declares, say, only 200/400 in responses, with a handler that returns an undeclared status and a map body:

return router:response(500, (), map { "error": "something unexpected" }, ())

(or any error that roaster/the app maps to a status the spec doesn't list). The client receives SENR0001 rather than a JSON 500.

Why it's a footgun

It's a cryptic failure for a routine situation — the spec describes the intended responses, but real handlers hit unanticipated statuses (raw exceptions mapped to 500, etc.). The current behavior turns "responded with an undeclared status" into "could not serialize the body," which is hard to connect back to the cause.

Proposed fix (happy to PR)

A few options, roughly in order of preference:

  1. Body-aware fallback — in router:write-response, if no content-type resolves for the status but the body is a map(*)/array(*), serialize as JSON (a map/array is never XML-serializable, so this can't regress an XML route). Lowest-risk.
  2. JSON default fallback — change the get-content-type-for-code fallback from application/xml to application/json for a JSON-first router. Simpler, slightly more opinionated.
  3. At minimum, a clearer error — raise a descriptive error naming the undeclared status / route instead of the downstream SENR0001.

(Today's workaround, for reference: a handler can pass an explicit media-type — router:response($code, "application/json", $body, ()) — which bypasses the fallback. That's what app code can do now, but a sane default would remove the footgun.)

Happy to open a PR for whichever direction you prefer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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