Between the moment a browser asks for a page and the moment it gets
an answer, brochure gives you two places to intervene. If you know
express.js, this is its middleware; in shiny
terms it is code that runs before any reactive context
exists.
The order things run in
- the browser sends a
GET, which R receives as areqobject - the app level
req_handlersrun, in order - the request is matched against your pages
- the matched page’s
req_handlersrun - R builds the
httpResponsefrom that page’s UI - the app level
res_handlersrun, then the matched page’s - the response is sent
A req_handler takes req and must return it.
A res_handler takes res and req,
and must return res. Both accept formulas: .x
is req for a request handler, and res for a
response handler, where .y is then req.
Answering before Shiny
A req_handler returning an httpResponse
short-circuits: that response is sent immediately, nothing else runs,
and no Shiny session is created. This is how you serve something that is
not a page — a healthcheck, a webhook receiver, a small JSON
endpoint:
brochureApp(
page(href = "/", ui = tagList(h1("Home"))),
page(
href = "/healthcheck",
ui = tagList(),
req_handlers = list(
~ shiny::httpResponse(200, content = "OK")
)
)
)The page still needs a ui, because page()
requires an href to bind to, but it is never rendered.
Note the asymmetry: an early httpResponse skips the
res_handlers too. If you rely on a response handler to set
a header on every answer, it will not see these.
Rejecting a request
Because a request handler can answer on its own, it can also refuse:
require_token <- function(req) {
token <- req$HTTP_X_TOKEN
if (is.null(token) || !identical(token, Sys.getenv("APP_TOKEN"))) {
return(shiny::httpResponse(401, content = "Unauthorized"))
}
req
}
brochureApp(
page(href = "/", ui = tagList(h1("Home"))),
req_handlers = list(require_token)
)This guards the HTTP request that serves the page. It does not guard the websocket the page opens afterwards, so treat it as a gate on the document, not as an authentication system.
Changing the response
brochureApp(
page(href = "/", ui = tagList(h1("Home"))),
res_handlers = list(
function(res, req) {
res$headers$`X-Frame-Options` <- "DENY"
res
}
)
)App level handlers run before page level ones, so a page can override what the app decided.
Page level or app level
An app level handler runs for every matched page. A page level one
runs only for its own page, which is what you want for anything specific
— a cookie for the login page, a Cache-Control for a page
that is expensive to build.
The app level request handlers are the exception to “for every
matched page”: they run before the request is matched at all, so they
see urls that match no page, and can answer them. Everything else — the
page level handlers, and both levels of response handler — is skipped
when nothing matches, and the request gets content_404
directly.
Pages that answer on another method
A page answers GET. method changes that,
which is how a form post or a webhook gets a page of its own:
subscribe <- function(req) {
fields <- shiny::parseQueryString(
rawToChar(req$rook.input$read())
)
message("new subscriber: ", fields$email)
req
}
brochureApp(
page(
href = "/",
ui = tagList(
tags$form(
action = "/subscribe",
method = "post",
tags$input(type = "email", name = "email"),
tags$input(type = "submit", value = "Subscribe")
)
)
),
page(
href = "/subscribe",
method = "POST",
ui = tagList(h1("Thanks")),
req_handlers = list(subscribe)
)
)The method takes part in the matching, so a GET on
/subscribe matches nothing and gets
content_404.
What was posted is yours to read: the body is on the request, as
req$rook.input, and brochure does not touch it. A request
handler is the place for that, since it runs before the response is
built.
A www/ directory at the root of your app breaks those
pages, and brochure cannot do anything about it: runApp()
mounts that directory as an httpuv static path on /, and
httpuv answers anything that is not a GET or a
HEAD from there with a 400 before R ever sees the request.
The request handler never runs. Serve those files under a prefix instead
— addResourcePath("assets", "assets") and
href = "assets/site.css" — which is what a
golem app already does.
One thing to keep in mind: a page’s session is resolved on
the path alone. The websocket handshake a browser opens is a
GET whatever method the page was served on, so two pages
declared at the same href with different methods both run the server of
the first one declared.
