Environment
- Plumber 1.3.3 (installed via CRAN snapshot 2026-07-12)
- R 4.4.2
- Reproduced inside Docker container in CI
Symptom
GET /health returns HTTP 200 with body {} (empty JSON object)
when the route is registered alongside an @filter decorator that
mutates res$status/res$body for rejections.
Expected: populated JSON body, e.g.
{"status":"ok","active_runs":0,"max_concurrent_runs":2,"memory_gb":6}
The response body becomes the empty object {} rather than the
serialized return value of the route handler.
Reproduction pattern
A router where:
- A @filter decorator exists. The filter:
- Inspects req$PATH_INFO
- Returns NULL for open endpoints (e.g. /health)
- Sets res$status and res$body and returns for auth failures
- A route handler returns a normal R list
- pr$setSerializer(serializer_json(auto_unbox = TRUE, na = "null")) is applied globally
Working workaround
Replace @filter with plumber::pr_hook(pr, "preroute", ...):
- On rejection: res$status <- ...; res$body <- ...; return(FALSE)
- On open endpoints: return(NULL) (continue chain)
- On success: return(NULL) (continue chain)
This pattern returns a populated response body in 1.3.3.
Hypothesis
The @filter decorator path appears to interact poorly with
serializer_json(auto_unbox = TRUE, na = "null") in 1.3.3 when the
filter mutates res$body. Possibly a serializer double-application
or a C++ callback bug in invokeCppCallback. The preroute hook path
does not appear to share the bug.
Observed in
https://github.com/unstable-branch/sdm-dashboard
Notes
We did not narrow this down to a minimal Plumber-only repro. Our
production setup also involves:
- A Postgres connection pool at startup
- The serializer_unboxed_json family
- OpenAPI docs toggled on via PLUMBER_DOCS_ENABLED=true
- An auth gate that runs on every request
All four of these may be required to trigger the bug. Happy to provide
more context or test patches if useful.
Environment
Symptom
GET /health returns HTTP 200 with body {} (empty JSON object)
when the route is registered alongside an @filter decorator that
mutates res$status/res$body for rejections.
Expected: populated JSON body, e.g.
{"status":"ok","active_runs":0,"max_concurrent_runs":2,"memory_gb":6}
The response body becomes the empty object {} rather than the
serialized return value of the route handler.
Reproduction pattern
A router where:
Working workaround
Replace @filter with plumber::pr_hook(pr, "preroute", ...):
This pattern returns a populated response body in 1.3.3.
Hypothesis
The @filter decorator path appears to interact poorly with
serializer_json(auto_unbox = TRUE, na = "null") in 1.3.3 when the
filter mutates res$body. Possibly a serializer double-application
or a C++ callback bug in invokeCppCallback. The preroute hook path
does not appear to share the bug.
Observed in
https://github.com/unstable-branch/sdm-dashboard
Notes
We did not narrow this down to a minimal Plumber-only repro. Our
production setup also involves:
All four of these may be required to trigger the bug. Happy to provide
more context or test patches if useful.