[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:
- 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.
- 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.
- 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.
[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 toapplication/xmland setsoutput:method=xml. If the body is amap(*)(the normal shape for a JSON API), serialization then fails with: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-responseresolves the content-type ashead(($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 usesget-content-type-for-codewith anapplication/xmlfallback.router:get-content-type-for-codelooks upresponses[string($code)](orresponses.default); if the status isn't declared and there's nodefault, it returns theapplication/xmlfallback.router:write-responsethen setsContent-Type: application/xmlandutil:declare-option("output:method", "xml"). Amap(*)/array(*)body cannot be serialized with the XML method →SENR0001.Minimal repro
A route that declares, say, only
200/400inresponses, with a handler that returns an undeclared status and a map body:(or any error that roaster/the app maps to a status the spec doesn't list). The client receives
SENR0001rather than a JSON500.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:
router:write-response, if no content-type resolves for the status but the body is amap(*)/array(*), serialize as JSON (a map/array is never XML-serializable, so this can't regress an XML route). Lowest-risk.get-content-type-for-codefallback fromapplication/xmltoapplication/jsonfor a JSON-first router. Simpler, slightly more opinionated.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.