knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
library(shiny) library(brochure)
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.
GET, which R receives as a req objectreq_handlers run, in orderreq_handlers runhttpResponse from that page's UIres_handlers run, then the matched page'sA 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.
log_where <- function(req) { message(Sys.time(), " - ", req$PATH_INFO) req } brochureApp( page(href = "/", ui = tagList(h1("Home"))), req_handlers = list(log_where) )
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.
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.
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.
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.
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.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.