The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
First stable release. Routing is rewritten on top of
{routr}, which brings parameterised routes and per-method
pages, and fixes a family of bugs that came from keeping the current
page in shared state.
keys is gone. A parameterised page used to read its
matched values from a keys object that
brochureApp() installed in the global environment: it
shadowed any user object of that name and was invisible from a module, a
golem app or any local(). Use get_keys()
instead — with no argument inside a page server, and
get_keys(request) inside a page ui.
set_cookie() now defaults to
http_only = TRUE and same_site = "Lax". Pass
http_only = FALSE or same_site = NULL to get
the previous header.
set_cookie(), remove_cookie() and
server_redirect() reject values they used to interpolate as
is: a cookie name, value, domain or path outside the RFC 6265 charset,
and a redirect target that is neither a path nor an http(s)
URL.
... in brochureApp() no longer accepts
anything. It takes page()s, redirect()s, tags,
tag lists, html dependencies and strings; anything else is an error
rather than content silently injected into every page. A bare list of
pages is spliced, so brochureApp(list(page_1(), page_2()))
now builds two pages instead of injecting the list into each of
them.
page() and redirect() gain a
method argument, so a page can answer on something other
than GET.
Two people browsing two pages of the same app could each run the other’s server. The matched page was kept in a single per-app environment, written by the http handler and read back when the websocket opened. The page is now resolved per request in the ui, and per session in the server.
An unknown url served the last page visited instead of
content_404.
req_handlers and res_handlers passed to
page() were stored and never run: the documented
/healthcheck endpoint and the per-page cookies of the
README were silent no-ops.
A page whose href had more than one segment (any parameterised
route) loaded none of its assets: Shiny emits its dependencies as
relative urls, and the browser resolved them against the parent path.
With no shiny.js there was no websocket, so the page server
never ran.
basepath is now removed from the incoming path, as
documented, and not only prepended to the urls the app emits. Apps work
behind a proxy that passes the mount through as well as behind one that
strips it.
remove_cookie() could not delete a cookie set with
an explicit path or domain. A cookie is only
replaced when name, path and domain all match, and the deletion header
carried none of them, so the browser scoped it to the directory of the
current request and left the original cookie alone. Behind a mount that
directory is not /, so the documented login/logout pattern
silently failed on Posit Connect. It now takes path and
domain.
Pages and redirects are matched in the order they were passed.
Redirects were registered after every page whatever the order, so a
redirect() declared before a page() at the
same href never answered.
redirect() accepts only the statuses a browser
follows: 301, 302, 303, 307 and 308. The allow-list read
c(301:308, 310), which admitted 304, 305, 306 and 310 —
none of which make a browser follow a Location.
basepath has to be a plain url path. It is
interpolated into urls and into the injected javascript, where a quote
in it closed the string literal.
Mount rewriting is blind to capitalisation, so
<IMG SRC="logo.png"> is rewritten like its lowercase
equivalent.
An explicit basepath now decides the mount even on
Posit Connect. The header used to win, so an app that set
basepath to pin the behaviour was stripping the incoming
path with one mount and writing its urls with another.
server_redirect() and redirect() refuse
a backslash anywhere in the target. A URL parser reads it as a slash, so
"\page2" was the root and escaped the mount, which is only
ever prefixed onto a /.
Mount rewriting handles single quoted attributes, so
HTML("<img src='logo.png'>") is rewritten like its
double quoted equivalent.
A page declared with a method other than GET can
serve its document. Shiny’s UI handler answers GET alone
unless the ui says otherwise, so such a page returned nothing unless a
request handler answered for it.
server_redirect() and redirect() refuse
an empty target, which is neither a path nor a url: the browser dropped
it silently, and the HTTP redirect emitted an empty
Location.
set_cookie() checks max_age, the one
part still interpolated into the header unexamined:
max_age = "0\r\nX-Injected: 1" appended a header of its
own.
server_redirect() refuses a protocol relative target
written with backslashes. A URL parser reads \ as
/, so "\\host" and "/\host" left
the app exactly as "//host" did.
Mount rewriting only touches real attributes, and covers form
action. It matched anywhere in the response, so page text
or inline javascript holding src="logo.png" was rewritten
too; and a form posting to /submit under a mount went to
the domain root.
redirect() did not check to, which goes
straight into a Location header: a CRLF in it appended a
header of its own. It is now held to the same rule as
server_redirect().
An internal redirect() sent the browser out of the
app when it was mounted under a prefix. The redirect answers before
anything is rewritten, so its Location was emitted
unprefixed.
A page declared with a method other than GET never
ran its server. The websocket handshake is a GET whatever
the page answers on, so it matched no route; sessions are now resolved
against the path alone.
A root absolute resource url such as src="/img.png"
was left outside the app under a mount, while a relative navigation link
such as <a href="contact"> was rewritten to
/contact, moving where its author pointed it. Resource urls
and navigation links are now treated separately.
Two apps in the same R process shared their app level
req_handlers and res_handlers, and the last
one built won.
Formulas now work as app level handlers, as the README says they do; only page level handlers converted them.
An app behind a proxy that passes its mount through – shinyProxy,
or an nginx proxy_pass without a trailing slash – loaded
none of its assets. Shiny’s resource paths are served by httpuv before R
and matched against the raw path, so
/myapp/jquery-3.7.1/jquery.min.js matched no static path
and landed on brochure, which answered content_404. Without
shiny.js there was no websocket, so no page was ever
interactive. Such a request now falls through to the handler Shiny keeps
for it. Checked from a browser against inst/subpage behind
both kinds of proxy: 41 checks pass on each, where the pass-through one
failed on the first page.
/reactlog is served again. Every path brochure did
not route got content_404, and Shiny’s own handler for that
endpoint comes after the app’s in the chain, so
options(shiny.reactlog = TRUE) and Ctrl+F3 answered
404.
Two pages carrying two bslib::bs_theme()s no longer
fight over one url. Shiny builds a dependency’s url prefix from its name
and version, and both themes ship bootstrap at the same
version from two different compiled directories: the page rendered last
took the prefix, and the browser then styled pages with whichever css it
had cached under it. Each source directory now gets its own
prefix.
The script brochure injects now only registers the handler behind
server_redirect(). It also used to rewrite the page’s
<base>, on the premise that the websocket url was
computed from it; shiny builds that url from
window.location.pathname instead. Checked on a Posit
Connect deployment: stripping the script from the response left a deep
page with the same websocket url, the same rendered output and no failed
request.
get_mount() reads the mount from the
RStudio-Connect-App-Base-URL header only when the app
really runs on Posit Connect, and keeps the extracted path to a plain
path. Elsewhere any client could send that header and decide the prefix
every url of the page is rewritten with.
The {routr} dependency is on the CRAN release, and
{uuid} and {fastmap} are no longer
needed.
golem_hook() leaves a tested app: it writes a test
for the page it creates, along with the testthat structure to run it
under R CMD check, and adds brochure to the Imports of the
app it hooks – the module template calls page(), so nothing
the hook wrote could be loaded or tested without it.
The golem recommended tests come in a brochure version, and
golem::use_recommended_tests() is taken out of
dev/01_start.R. The ones golem writes drive
app_ui() and app_server(), which the hook
deletes: a freshly created app failed its own test suite. What replaces
them is the app object and the page it serves on
/.
vignette("design") says how a brochure app behaves
with several visitors at once: a session is one visitor on one page,
they all share one R process, and a page that blocks holds up every
other page.
vignette("handlers") and ?page say why
a page on a method other than GET is unreachable when the
app has a www/ directory at its root: Shiny mounts it as a
static path on /, and httpuv answers anything that is not a
GET or a HEAD from there with a 400 before R
sees the request.
Seven vignettes and a pkgdown site.
vignette("brochure") gets you started,
vignette("handlers") covers the request and response
middleware, vignette("cookies") the cookie API and carrying
a session between pages, vignette("design") what changes
when an app is several pages, vignette("deployment")
serving the app under a prefix, vignette("testing") testing
the routing and the pages, and vignette("golem") building a
brochure app as a golem package.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.