Deployment platforms decide whether your application is working by probing an HTTP endpoint:
A failing liveness probe restarts your container; a failing readiness
probe stops routing traffic to it. Shiny has no native concept of a
health endpoint, and the common workarounds (wrapping the UI function,
adjusting uiPattern, running a plumber sidecar) are easy to
get wrong – a misconfigured uiPattern silently 404s every
probe, and a health check behind an authentication wrapper bounces
probes to a sign-in page.
shinyhealth solves this at the right layer: the app
object’s own HTTP handler, where requests can be intercepted before any
UI rendering, session creation, or authentication.
Pipe any Shiny app object through add_healthcheck():
app <- shiny::shinyApp(
ui = shiny::fluidPage("hello"),
server = function(input, output, session) {}
) |>
add_healthcheck()
has_healthcheck(app)
#> [1] TRUE
healthcheck_paths(app)
#> [1] "/health"The decorated app answers GET /health with a JSON
response and passes every other request through to your app unchanged.
No uiPattern adjustment is needed, and construction method
does not matter – plain shiny::shinyApp(), golem, or apps
wrapped by authentication packages like polished all work, because the
interception happens after the app object is fully constructed.
Probing the endpoint returns:
health_response() builds the response objects served
from health endpoints. The defaults produce a healthy response:
Two fields are deliberately independent:
status_code (the HTTP transport
status) is what orchestrators interpret. 200 means healthy;
503 (Service Unavailable) is the standard choice for
unhealthy.status (the body field) is a
convention for humans and tooling. The default vocabulary
("pass", and by convention "warn" /
"fail") follows the IETF
health check response format draft, but any value is accepted.The response defaults follow the relevant standards: application/health+json
content type (safe for any JSON consumer per RFC 6839’s
+json suffix rule), Cache-Control: no-store
so probe responses are never cached, and RFC 3339 UTC
timestamps.
Additional named fields merge into the body, and checks
carries arbitrarily nested check results:
A handler is any function that accepts the request object and returns one of:
health_response()shiny::httpResponse() (passed through
untouched)status_code field, if present, controls the HTTP
statushandler <- function(req) {
list(status = "degraded", status_code = 503L, reason = "cache warming")
}
as_health_response(handler(req = NULL))If a handler throws an error, the endpoint stays machine-readable:
the error is converted into a 503 response with
status = "fail" and the condition message under
output.
Registrations stack, so separate endpoints with different handlers are just repeated calls:
app <- shiny::shinyApp(
ui = shiny::fluidPage("hello"),
server = function(input, output, session) {}
) |>
add_healthcheck(path = "/livez") |>
add_healthcheck(path = "/readyz", handler = function(req) {
health_response(
checks = list(database = list(status = "pass"))
)
})
healthcheck_paths(app)
#> [1] "/livez" "/readyz"A common pattern: keep the liveness handler trivial (the default – if the process can answer, it is alive) and put dependency checks on the readiness endpoint, so a database blip stops traffic routing without triggering container restarts.
add_healthcheck()’s verbose argument
(default: shiny::in_devmode())
logs each probe hit to the console running the app. Enable devmode
during development to watch probes arrive:
With the endpoint in place, point your platform’s probes at it. Kubernetes:
livenessProbe:
httpGet:
path: /livez
port: 8080
periodSeconds: 30
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 10Google
Cloud Run (gcloud run services update or service
YAML):
Azure
Container Apps (bicep/ARM probes block or the
portal):