| Title: | Easy and Powerful Web Servers |
|---|---|
| Description: | Automatically create a web server from annotated 'R' files or by building it up programmatically. Provides automatic 'OpenAPI' documentation, input handling, asynchronous evaluation, and plugin support. |
| Authors: | Thomas Lin Pedersen [aut, cre] (ORCID: <https://orcid.org/0000-0002-5147-4711>), Posit Software, PBC [cph, fnd] (ROR: <https://ror.org/03wc8by49>) |
| Maintainer: | Thomas Lin Pedersen <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0.9000 |
| Built: | 2026-05-21 10:15:29 UTC |
| Source: | https://github.com/posit-dev/plumber2 |
Package authors can extend plumber2 with their own functionalities. If they wish to add a new tag to be used when writing annotated plumber2 routes they can use this function. If so, it should be called when the package is loaded.
add_plumber2_tag(tag, handler = NULL)add_plumber2_tag(tag, handler = NULL)
tag |
The name of the tag |
handler |
A handler function for the tag. See Details |
The handler argument must be, if provided, a function with the arguments
block, call, tags, values, and env. block is a list with the
currently parsed information from the block. You can add or modify the values
within to suit your need as well as subclass it. You should not remove any
values as others might need them. call is the parsed value of whatever
expression was beneath the plumber2 block. tags is a character vector of
all the tags in the block, and values is a list of all the values
associated with the tags (that is, whatever comes after the tag in the
block). The values are unparsed. You should assume that all tags not relevant
for your extension has already been handled and incorporated into block.
The env argument contains the environment the annotation file is evaluated
in. The function must return a modified version of block unless block is
of the class plumber2_empty_block in which case it is allowed to construct
a new object from scratch. If you add a subclass to block you should make
sure that a method for apply_plumber2_block() for the subclass exists.
If handler is NULL then the tag will be registered but no associated
handler will be added. This can make sense if you have a new block type that
consists of multiple tags but only want a single handler for it. In that case
you register a handler for one of the required tags and register the
remaining tags without a handler.
This function is called for its side effects
# Add a tag that says hello when used add_plumber2_tag("hello", function(block, call, tags, values, env) { message("Hello") class(block) <- c("hello_block", class(block)) block })# Add a tag that says hello when used add_plumber2_tag("hello", function(block, call, tags, values, env) { message("Hello") class(block) <- c("hello_block", class(block)) block })
This is the main way to create a new Plumber2 object that encapsulates your
full api. It is also possible to add files to the API after creation using
api_parse()
api( ..., host = get_opts("host", "127.0.0.1"), port = get_opts("port", 8080), doc_type = get_opts("docType", "rapidoc"), doc_path = get_opts("docPath", "__docs__"), reject_missing_methods = get_opts("rejectMissingMethods", FALSE), ignore_trailing_slash = get_opts("ignoreTrailingSlash", TRUE), max_request_size = get_opts("maxRequestSize"), shared_secret = get_opts("sharedSecret"), compression_limit = get_opts("compressionLimit", 1000), default_async = get_opts("async", "mirai"), env = caller_env() ) is_plumber_api(x) api_parse(api, ...)api( ..., host = get_opts("host", "127.0.0.1"), port = get_opts("port", 8080), doc_type = get_opts("docType", "rapidoc"), doc_path = get_opts("docPath", "__docs__"), reject_missing_methods = get_opts("rejectMissingMethods", FALSE), ignore_trailing_slash = get_opts("ignoreTrailingSlash", TRUE), max_request_size = get_opts("maxRequestSize"), shared_secret = get_opts("sharedSecret"), compression_limit = get_opts("compressionLimit", 1000), default_async = get_opts("async", "mirai"), env = caller_env() ) is_plumber_api(x) api_parse(api, ...)
... |
plumber files or directories containing plumber files to be parsed
in the given order. The order of parsing determines the final order of the
routes in the stack. If |
host |
A string that is a valid IPv4 address that is owned by this server |
port |
A number or integer that indicates the server port that should be listened on. Note that on most Unix-like systems including Linux and macOS, port numbers smaller than 1024 require root privileges. |
doc_type |
The type of API documentation to generate. Can be either
|
doc_path |
The URL path to serve the api documentation from |
reject_missing_methods |
Should requests to paths that doesn't
have a handler for the specific method automatically be rejected with a
405 Method Not Allowed response with the correct Allow header informing
the client of the implemented methods. Assigning a handler to |
ignore_trailing_slash |
Logical. Should the trailing slash of a path
be ignored when adding handlers and handling requests. Setting this will
not change the request or the path associated with but just ensure that
both |
max_request_size |
Sets a maximum size of request bodies. Setting this
will add a handler to the header router that automatically rejects requests
based on their |
shared_secret |
Assigns a shared secret to the api. Setting this will
add a handler to the header router that automatically rejects requests if
their |
compression_limit |
The size threshold in bytes for trying to compress the response body (it is still dependant on content negotiation) |
default_async |
The default evaluator to use for async request handling |
env |
The parent environment to the environment the files should be evaluated in. Each file will be evaluated in it's own environment so they don't interfere with each other |
x |
An object to test for whether it is a plumber api |
api |
A plumber2 api object to parse files into |
A Plumber2 object
api_package() for creating an api based on files distributed with
a package
get_opts() for how to set default options
# When creating an API programmatically you'll usually initialise the object # without pointing to any route files or a _server.yml file pa <- api() # You can pass it a directory and it will load up all recognised files it # contains example_dir <- system.file("plumber2", "quickstart", package = "plumber2") pa <- api(example_dir) # Or you can pass files directly pa <- api(list.files(example_dir, full.names = TRUE)[1])# When creating an API programmatically you'll usually initialise the object # without pointing to any route files or a _server.yml file pa <- api() # You can pass it a directory and it will load up all recognised files it # contains example_dir <- system.file("plumber2", "quickstart", package = "plumber2") pa <- api(example_dir) # Or you can pass files directly pa <- api(list.files(example_dir, full.names = TRUE)[1])
This function allows explicit creation of routes or addition/merging of a
predefined routr::Route into the router of the api. A new route can also be
created with the route argument when adding a handler. However,
that way will always add new routes to the end of the stack, whereas using
api_add_route() allows you full control of the placement.
api_add_route(api, name, route = NULL, header = FALSE, after = NULL, root = "")api_add_route(api, name, route = NULL, header = FALSE, after = NULL, root = "")
api |
A plumber2 api object to add the route to |
name |
The name of the route to add. If a route is already present with this name then the provided route (if any) is merged into it |
route |
The route to add. If |
header |
Logical. Should the route be added to the header router? |
after |
The location to place the new route on the stack. |
root |
The root path to serve this route from. |
This functions return the api object allowing for easy chaining
with the pipe
There is no direct equivalent to this when using annotated route files.
However you can name your route in a file by adding @routeName <name> to
the first block of the file like so.
#* @routeName my_route NULL
All relevant blocks in the file will then be added to this route, even if the route already exist. In that way you can split the definition of a single route out among multiple files if needed.
# Add a new route and use it for a handler api() |> api_add_route("logger_route") |> api_any( "/*", function() { cat("I just handled a request!") }, route = "logger_route" )# Add a new route and use it for a handler api() |> api_add_route("logger_route") |> api_any( "/*", function() { cat("I just handled a request!") }, route = "logger_route" )
plumber2 provides two ways to serve files from your server. One
(api_assets) goes through R and gives you all the power you expect to
further modify and work with the response. The other (api_statics) never hits
the R process and as a result is blazing fast. However this comes with the
price of very limited freedom to modify the response or even do basic
authentication. Each has their place.
api_assets( api, at, path, default_file = "index.html", default_ext = "html", finalize = NULL, continue = FALSE, auth_flow = NULL, auth_scope = NULL, route = NULL ) api_statics( api, at, path, use_index = TRUE, fallthrough = FALSE, html_charset = "utf-8", headers = list(), validation = NULL, except = NULL )api_assets( api, at, path, default_file = "index.html", default_ext = "html", finalize = NULL, continue = FALSE, auth_flow = NULL, auth_scope = NULL, route = NULL ) api_statics( api, at, path, use_index = TRUE, fallthrough = FALSE, html_charset = "utf-8", headers = list(), validation = NULL, except = NULL )
api |
A plumber2 api object to add the rossource serving to |
at |
The path to serve the resources from |
path |
The location on the file system to map |
default_file |
The default file to look for if the path does not map to a file directly (see Details) |
default_ext |
The default file extension to add to the file if a file cannot be found at the provided path and the path does not have an extension (see Details) |
finalize |
An optional function to run if a file is found. The function
will receive the request as the first argument, the response as the second,
and anything passed on through |
continue |
A logical that should be returned if a file is found.
Defaults to |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
route |
The name of the route in the header router to add the asset route to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack |
use_index |
Should an |
fallthrough |
Should requests that doesn't match a file enter the request loop or have a 404 response send directly |
html_charset |
The charset to report when serving html files |
headers |
A list of headers to add to the response. Will be combined with the global headers of the app |
validation |
An optional validation pattern. Presently, the only type of
validation supported is an exact string match of a header. For example, if
|
except |
One or more url paths that should be excluded from the route.
Requests matching these will enter the standard router dispatch. The paths
are interpreted as subpaths to |
These functions return the api object allowing for easy chaining
with the pipe
When using annotated route files the functionality of api_assets() can be
achieved like this:
#* @assets my_wd/ ./ NULL
When using annotated route files the functionality of api_statics() can be
achieved like this:
#* @statics my_docs/ ~/ #* @except my_secret_folder/ NULL
# Add asset serving through routr route api() |> api_assets("my_wd/", "./") # Add asset serving directly api() |> api_statics("my_docs", "~/", except = "my_secret_folder/")# Add asset serving through routr route api() |> api_assets("my_wd/", "./") # Add asset serving directly api() |> api_statics("my_docs", "~/", except = "my_secret_folder/")
This function adds auth to a specific method + path. It does so by
defining an auth flow which the request must pass in order to proceed, as
well as an optional vector of scopes required. The flow is given as a logical
expression of guards it must satisfy. If you
have registered two guards, auth1 and auth2, then a flow could be
auth1 && auth2 to require that both guards must be passed to gain
access. Alternatively you could use auth1 || auth2 to require that just one
of them are passed. Flows can be arbitrarily complex with nesting etc, but
the OpenAPI spec has limits to what it can describe so if you want to have an
OpenAPI compliant api you must limit yourself to at most two levels of
nesting with the outer level being || (ie.
(auth1 && auth2) || (auth3 && auth4) is ok, but
(auth1 || auth2) && (auth3 || auth4) is not due to the outer level being
&&, and (auth1 && auth2) || (auth3 && (auth4 || auth5)) is not allowed
because it has 3 nesting levels). This is only a limitation of OpenAPI and
plumber2 itself can handle all of the above. If scope is given the scope of
the user after successful authentication must contain all of the provided
scopes. While this function allows you to add authentication to a path
directly, it is often more convenient to add it along with the resource or
functionality you want to protect. To that end, many functions such as
api_get() and api_report() also takes auth_flow and auth_scope as
input and if given will add auth to the relevant endpoint.
api_auth(api, method, path, auth_flow, auth_scope = NULL, add_doc = TRUE)api_auth(api, method, path, auth_flow, auth_scope = NULL, add_doc = TRUE)
api |
A plumber2 api object to add authentication to |
method |
The HTTP method to add authentication to |
path |
A string giving the path to be authenticated |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
add_doc |
Should OpenAPI documentation be added for the authentication |
This functions return the api object allowing for easy chaining
with the pipe
# We are not adding the guards here - only the auth flow # We assume the guards `oauth`, `basic`, and `key` will be added # later api() |> api_datastore(storr::driver_environment()) |> api_auth( method = "get", path = "/user/<username>", auth_flow = oauth || (basic && key) )# We are not adding the guards here - only the auth flow # We assume the guards `oauth`, `basic`, and `key` will be added # later api() |> api_datastore(storr::driver_environment()) |> api_auth( method = "get", path = "/user/<username>", auth_flow = oauth || (basic && key) )
This function adds an auth guard to your API. Notably, this does
not turn on auth for any of your handlers but makes it available
for reference in an auth flow. To use it, reference it in the
auth_flow argument of functions supporting it. Guards are
defined using the various guard_*() constructors in the fireproof package.
Refer to these for further documentation
api_auth_guard(api, guard, name = NULL)api_auth_guard(api, guard, name = NULL)
api |
A plumber2 api object to add the authenticator to |
guard |
A Guard subclass object defining an authentication scheme |
name |
The name to use for referencing the guard in an authentication flow |
This functions return the api object allowing for easy chaining
with the pipe
To add a guard to your api defined in an annotated file use the
@authGuard tag:
#* @authGuard BasicAuth fireproof::guard_basic(...)
The tag parameter (BasicAuth) provides the name for the guard
guard <- fireproof::guard_key( key_name = "plumber2-key", validate = "MY_VERY_SECRET_KEY" ) api() |> api_datastore(storr::driver_environment()) |> api_auth_guard(guard, "cookie_key")guard <- fireproof::guard_key( key_name = "plumber2-key", validate = "MY_VERY_SECRET_KEY" ) api() |> api_datastore(storr::driver_environment()) |> api_auth_guard(guard, "cookie_key")
While using a session cookie is a convenient solution to persistent data storage between requests it has the downside of requiring the data to be passed back and forth between server and client at every exchange. This makes it impractical for all but the smallest snippets of data. An alternative strategy is to use server-side storage which this function facilitates. It uses the firesale plugin under the hood to provide a list-like interface to a storr-backed key-value store. storr in turn provides interfaces to a range of backends such as redis, LMDB, and databases supported by DBI. Further it provides simpler (but setup-free) solutions such as using an environment (obviously less persistent) or a folder of rds files.
api_datastore( api, driver, store_name = "datastore", gc_interval = 3600, max_age = gc_interval )api_datastore( api, driver, store_name = "datastore", gc_interval = 3600, max_age = gc_interval )
api |
A plumber2 api object to add the datastore setup to |
driver |
A storr compatible driver that defines the backend of the datastore |
store_name |
The argument name under which the datastore will be available to the request handlers |
gc_interval |
The interval between running garbage collection on the backend |
max_age |
The time since last request to pass before a session store is cleared |
Once you turn the datastore on with this function your request handlers will
gain access to a new argument (defaults to datastore but this can be
changed with the store_name argument). The datastore argument will
contain a list holding two elements: global and session which in turn
will be list-like interfaces to the underlying key-value store. The global
element access a store shared by all sessions whereas the session element
is scoped to the current session. Depending on the value of max_age the
session specific data is purged once a certain amount of time has passed
since the last request from that session.
These functions return the api object allowing for easy chaining
with the pipe
You can define a datastore backend using the @datastore tag and provide the
driver specification below the block
#* @datastore storr::driver_dbi(...)
api() |> api_datastore(storr::driver_environment()) |> api_get("hello", function(datastore) { if (length(datastore$session) == 0) { datastore$global$count <- (datastore$global$count %||% 0) + 1 datastore$session$not_first_visit <- TRUE paste0("Welcome. You are visitor #", datastore$global$count) } else { "Welcome back" } })api() |> api_datastore(storr::driver_environment()) |> api_get("hello", function(datastore) { if (length(datastore$session) == 0) { datastore$global$count <- (datastore$global$count %||% 0) + 1 datastore$session$not_first_visit <- TRUE paste0("Welcome. You are visitor #", datastore$global$count) } else { "Welcome back" } })
The OpenAPI standard offers a way to describe the
various endpoints of your api in machine- and human-readable way. On top of
this, various solutions have been build to generate online documentation of
the API based on a provided OpenAPI spec. plumber2 offers support for
RapiDoc, Redoc, and
Swagger as a UI frontend for the documentation and will
also generate the spec for you based on the tags in parsed files. If you are
creating your API programmatically or you wish to add to the autogenerated
docs you can add docs manually, either when adding a handler (using the doc
argument), or with the api_doc_add() function
api_doc_setting(api, doc_type, doc_path, ...) api_doc_add(api, doc, overwrite = FALSE, subset = NULL)api_doc_setting(api, doc_type, doc_path, ...) api_doc_add(api, doc, overwrite = FALSE, subset = NULL)
api |
A plumber2 api object to add docs or doc settings to |
doc_type |
The type of API documentation to generate. Can be either
|
doc_path |
The URL path to serve the api documentation from |
... |
plumber files or directories containing plumber files to be parsed
in the given order. The order of parsing determines the final order of the
routes in the stack. If |
doc |
A list with the OpenAPI documentation, usually constructed with one of the helper functions |
overwrite |
Logical. Should already existing documentation be
removed or should it be merged together with |
subset |
A character vector giving the path to the subset of the
docs to assign |
These functions return the api object allowing for easy chaining
with the pipe
When using annotated route files documentation is automatically generated based on the annotation. The following tags will contribute to documentation:
@title
@description
@details
@tos
@license
@contact
@tag
@param
@query
@body
@response
@parsers
@serializers
Documentation is only generated for annotations related to global
documentation (a block followed by the "_API" sentinel), request handlers
(a block including one of @get, @head, @post, @put, @delete,
@connect, @options, @trace, @patch, or @any), or report generation
(a block including @report)
# Serve the docs from a different path api() |> api_doc_setting(doc_path = "__man__") # Add documentation to the api programmatically api() |> api_doc_add(openapi( info = openapi_info( title = "My awesome api", version = "1.0.0" ) )) # Add documentation to a subset of the docs api() |> api_doc_add( openapi_operation( summary = "Get the current date", responses = list( "200" = openapi_response( description = "Current Date", content = openapi_content( "text/plain" = openapi_schema(character()) ) ) ) ), subset = c("paths", "/date", "get") )# Serve the docs from a different path api() |> api_doc_setting(doc_path = "__man__") # Add documentation to the api programmatically api() |> api_doc_add(openapi( info = openapi_info( title = "My awesome api", version = "1.0.0" ) )) # Add documentation to a subset of the docs api() |> api_doc_add( openapi_operation( summary = "Get the current date", responses = list( "200" = openapi_response( description = "Current Date", content = openapi_content( "text/plain" = openapi_schema(character()) ) ) ) ), subset = c("paths", "/date", "get") )
You can set up your plumber2 api to act as reverse proxy and forward all
requests to a specific path (and it's subpaths) to a different URL. In
contrast to api_shiny(), api_forward() is not responsible for launching
whatever service is being proxied so this should be handled elsewhere. The
path will be stripped from the request before being forwarded to the url,
meaning that if you set up a proxy on my/proxy/ to http://example.com,
then a request for my/proxy/user/thomas will end at
http://example.com/user/thomas. Proxying is most useful when forwarding to
internal servers though you are free to forward to public URLs as well.
However, for the later you'd usually use a redirect instead (via
api_redirect())
api_forward(api, path, url, except = NULL, auth_flow = NULL, auth_scope = NULL)api_forward(api, path, url, except = NULL, auth_flow = NULL, auth_scope = NULL)
api |
A plumber2 api to add the shiny app to |
path |
The path to serve the shiny app from |
url |
The url to forward to |
except |
Subpaths to |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
This functions return the api object allowing for easy chaining
with the pipe
You can set up a reverse proxy in your annotated route file using the
@forward tag
#* @forward /proxy http://127.0.0.1:56789 NULL
# Serve wikipedia directly from your app api() |> api_forward("my_wiki/", "https://www.wikipedia.org")# Serve wikipedia directly from your app api() |> api_forward("my_wiki/", "https://www.wikipedia.org")
plumber2 has a build-in logging facility that takes care of logging any
conditions that are caught, as well as access logs. Further it is possible to
log custom messages using the log() method on the api object. However, the
actual logging is handled by a customizable function that can be set. You can
read more about the logging infrastructure in the
fiery documentation. plumber2 reexports the loggers
provided by fiery so they are immediately available to the user.
api_logger(api, logger = NULL, access_log_format = NULL) logger_null() logger_console(format = "{time} - {event}: {message}") logger_file(file, format = "{time} - {event}: {message}") logger_logger(default_level = "INFO") logger_otel(format = "{message}") logger_switch(..., default = logger_null()) common_log_format combined_log_formatapi_logger(api, logger = NULL, access_log_format = NULL) logger_null() logger_console(format = "{time} - {event}: {message}") logger_file(file, format = "{time} - {event}: {message}") logger_logger(default_level = "INFO") logger_otel(format = "{message}") logger_switch(..., default = logger_null()) common_log_format combined_log_format
api |
A plumber2 api object to set the logger on |
logger |
A logger function. If |
access_log_format |
A glue string giving the format for the access logs.
plumber2 (through fiery) provides the predefined |
format |
A glue-like string specifying the format of the
log entry. Only the variables |
file |
A file or connection to write to |
default_level |
The log level to use for events that are not |
... |
A named list of loggers to use for different events. The same
semantics as switch is used so it is possible to let events
fall through e.g. |
default |
A catch-all logger for use with events not defined in |
Logger setup doesn't have a dedicated annotation tag, but you can set it up
in a @plumber block
#* @plumber
function(api) {
api |>
api_logger(logger = logger_null())
}
# Use a different access log format api() |> api_logger(access_log_format = combined_log_format) # Turn off logging api() |> api_logger(logger_null())# Use a different access log format api() |> api_logger(access_log_format = combined_log_format) # Turn off logging api() |> api_logger(logger_null())
WebSockets is a bidirectional communication channel that can be established at the request of the client. While websocket communication is not really part of a standard REST api, it has many uses and can easily be used together with one.
api_message(api, handler, async = NULL, then = NULL)api_message(api, handler, async = NULL, then = NULL)
api |
A plumber2 api object to add the handler to |
handler |
A function conforming to the specifications laid out in Details |
async |
If |
then |
A list of function to be called once the async handler is done.
The functions will be chained using |
A handler for a websocket message is much simpler than for requests in general since it doesn't have to concern itself with methods, paths, and responses. Any message handler registered will get called in sequence when a websocket message is received from a client. Still, a few expectations apply
The handler can take any of the following arguments:
message: Either a raw vector if the message received is in binary form or
a single string, giving the message sent from the client
server: The Plumber2 object representing your server implementation
client_id: A string uniquely identifying the session the request comes
from
request: The request that was initially used to establish the websocket
connection with the client as a reqres::Request object
It is not expected that a websocket message sends a response and thus the handler is not required to do anything like that. However, if the handler returns either a raw vector or a single string it is taken as a signal to send this back to the client. Any other return value is silently ignored.
This functions return the api object allowing for easy chaining
with the pipe
You can handle websocket messages asynchronously if needed. Like with
request handlers you can either do it manually by creating and
returning a promise inside the handler, or by letting plumber2 convert your
handler to an async handler using the async argument. Due to the nature of
promises a handler being converted to a promise can't take request and
server arguments, so if you need to manipulate these you need to use then
(more on this shortly). The same conventions about return value holds for
async message handlers as for regular ones.
Because you can't manipulate request or server in the async handler it
may be needed to add operations to perform once the async handler has
finished. This can be done through the then argument. This takes a list of
functions to chain to the promise using promises::then(). Before the then
chain is executed the return value of the async handler will be send back to
the client if it is a string or a raw vector. Each then call will receive
the same arguments as a standard message handler as well as result which
will hold the return value of the previous handler in the chain. For the
first then call result will be whatever the main async handler returned.
The return value of the last call in the chain will be silently ignored.
A websocket message handler can be added to an API in an annotated route file
by using the @message tag
#* @message
function(message) {
if (message == "Hello") {
return("Hello, you...")
}
}
You can create async handlers with then chaining using annotation, through
the @async and @then tags
#* @message
#* @async
function(message) {
if (message == "Hello") {
return("Hello, you...")
}
}
#* @then
function(server) {
server$log("message", "websocket message received")
}
api() |> api_message( function(message) { if (message == "Hello") { return("Hello, you...") } } )api() |> api_message( function(message) { if (message == "Hello") { return("Hello, you...") } } )
During the life cycle of a plumber API various events will be fired, either
automatically or manually. See the article on events in fiery
for a full overview. api_on() allows you to add handlers that are called
when specific events fire. api_off() can be used to remove the handler if
necessary
api_on(api, event, handler, id = NULL) api_off(api, id)api_on(api, event, handler, id = NULL) api_off(api, id)
api |
A plumber2 api object to launch or stop |
event |
A string naming the event to listen for |
handler |
A function to call when |
id |
A string uniquely identifying the handler. If |
These functions return the api object allowing for easy chaining
with the pipe
Event handler setup doesn't have a dedicated annotation tag, but you can set
it up in a @plumber block
#* @plumber
function(api) {
api |>
api_on("cycle-end", function(server) {
server$log("message", "tick-tock")
})
}
# Add a small console log to show the api is alive pa <- api() |> api_on("cycle-end", function(server) { server$log("message", "tick-tock") }, id = "lifesign") # Remove it again pa |> api_off("lifesign")# Add a small console log to show the api is alive pa <- api() |> api_on("cycle-end", function(server) { server$log("message", "tick-tock") }, id = "lifesign") # Remove it again pa |> api_off("lifesign")
Packages can included one or more api specification(s) by storing the
annotated route files and/or _server.yml file in subfolders of
./inst/plumber2. The name of the subfolder will be the name of the api
api_package(package = NULL, name = NULL, ...)api_package(package = NULL, name = NULL, ...)
package |
The name of the package that provides the api. If |
name |
The name of the api. If |
... |
Arguments passed on to
|
If package or name is NULL then a data frame providing
available apis filtered on either package or name (if any is provided) is
returned. Otherwise a Plumber2 object representing the api is returned
# Load one of the plumber2 examples api_package("plumber2", "quickstart") # List all available apis api_package()# Load one of the plumber2 examples api_package("plumber2", "quickstart") # List all available apis api_package()
While it is optimal that an API remains stable over its lifetime it is often
not fully attainable. In order to direct requests for resources that has
been moved to the new location you can add a redirect that ensures a smooth
transition for clients still using the old path. Depending on the value
of permanent the redirect will respond with a 307 Temporary Redirect or
308 Permanent Redirect. from and to can contain path parameters and
wildcards which will be matched between the two to construct the correct
redirect path. Further, to can either be a path to the same server or a
fully qualified URL to redirect requests to another server altogether.
api_redirect(api, method, from, to, permanent = TRUE)api_redirect(api, method, from, to, permanent = TRUE)
api |
A plumber2 api object to add the redirect to |
method |
The HTTP method the redirect should respond to |
from |
The path the redirect should respond to |
to |
The path/URL to redirect the incoming request towards. After
resolving any path parameters and wildcards it will be used in the
|
permanent |
Logical. Is the redirect considered permanent or temporary? Determines the type of redirect status code to use |
This functions return the api object allowing for easy chaining
with the pipe
You can specify redirects in an annotated plumber file using the @redirect
tag. Precede the method with a ! to mark the redirect as permanent
#* @redirect !get /old/data/* /new/data/* #* @redirect any /unstable/endpoint /stable/endpoint NULL
api() |> api_redirect("get", "/old/data/*", "/new/data/*")api() |> api_redirect("get", "/old/data/*", "/new/data/*")
You can serve Quarto and Rmarkdown documents from a plumber2 api and have it automatically render the report when requested. Reports are automatically cached to reduce overhead. Parameterized reports are supported and parameters can be provided either with the query string for GET requests or in the request body for POST request. It is also possible to delete the cached render using a DELETE request. See Details for more information
api_report( api, path, report, ..., doc = NULL, max_age = Inf, async = TRUE, finalize = NULL, continue = FALSE, cache_dir = tempfile(pattern = "plumber2_report"), cache_by_id = FALSE, auth_flow = NULL, auth_scope = NULL, route = NULL )api_report( api, path, report, ..., doc = NULL, max_age = Inf, async = TRUE, finalize = NULL, continue = FALSE, cache_dir = tempfile(pattern = "plumber2_report"), cache_by_id = FALSE, auth_flow = NULL, auth_scope = NULL, route = NULL )
api |
A plumber2 api to serve the report with. |
path |
The base path to serve the report from. Additional endpoints will be created in addition to this. |
report |
The path to the report to serve |
... |
Further arguments to |
doc |
An |
max_age |
The maximum age in seconds to keep a rendered report before initiating a re-render |
async |
Should rendering happen asynchronously (using mirai) |
finalize |
An optional function to run before sending the response back. The function will receive the request as the first argument, the response as the second, and the server as the third. |
continue |
A logical that defines whether the response is returned directly after rendering or should be made available to subsequent routes |
cache_dir |
The location of the render cache. By default a temporary folder is created for it. |
cache_by_id |
Should caching be scoped by the user id. If the rendering is dependent on user-level access to different data this is necessary to avoid data leakage. |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
route |
The route this handler should be added to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack. |
Parameters provided to parameterized reports are automatically type checked
and casted based on the default values in the report and the schema provided
in the doc. Only the query parameters will be used as the request body is
inferred from that. It is important to understand that for Quarto documents,
the parameters are passed through as a yaml file and thus any type not
supported by yaml will not arrive unchanged to the document. Python reports
are supported, but the type of parameters cannot be inferred from the
document so if you want type checking you will have to provided schemas for
them in the doc. For POST request where the parameters are provided in the
body, you must use JSON format.
If a report has multiple different output formats then each format is
accessible through a subpath with the name of the format. The path provided
in path will use content negotiation through the Content-Type header to
select a format. In addition, a path with the file extension added to path
can also be used to select the specific format. For the last two, if multiple
formats share the same mime type/file extension then only the first one can
be selected.
Reports are cached, by default in a temporary folder. You can chose a
different folder with the cache_dir argument. Cached versions can be
deleted, thus forcing a rerender upon next request, by sending a DELETE
request. A DELETE request to the main path will delete all versions of the
report, whereas a DELETE request to one of the subpaths (see above) will only
delete versions of the specific output format. All different parameterized
versions will always be deleted together. Instead of sending DELETE requests
you can also set a max_age which will force a rerender if the render is
older than the given argument.
If the content of the report is dependent on different credentials given by the user you can cache the reports by session id so that every user will have it rendered uniquely for them. This is important to prevent leakage of confidential data, but also ensures that the report looks as expected for each user.
This functions return the api object allowing for easy chaining
with the pipe
A report can be served using an annotated route file by using the @report
tag and proceeding the annotation block with the path to the report
#* @report /quarterly "my/awesome/report.qmd"
api() |> api_report("/quarterly", "my/awesome/report.qmd")api() |> api_report("/quarterly", "my/awesome/report.qmd")
This family of functions facilitates adding a request handler for a specific HTTP method and path.
api_get( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_head( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_post( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_put( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_delete( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_connect( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_options( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_trace( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_patch( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_any( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL )api_get( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_head( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_post( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_put( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_delete( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_connect( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_options( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_trace( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_patch( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL ) api_any( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, auth_flow = NULL, auth_scope = NULL, then = NULL, doc = NULL, route = NULL )
api |
A plumber2 api object to add the handler to |
path |
A string giving the path the handler responds to. See Details |
handler |
A handler function to call when a request is matched to the path |
serializers |
A named list of serializers that can be used to format the
response before sending it back to the client. Which one is selected is based
on the request |
parsers |
A named list of parsers that can be used to parse the
request body before passing it in as the |
use_strict_serializer |
By default, if a serializer that respects the
requests |
download |
Should the response mark itself for download instead of being
shown inline? Setting this to |
async |
If |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
then |
A list of function to be called once the async handler is done.
The functions will be chained using |
doc |
A list with the OpenAPI spec for the endpoint |
route |
The route this handler should be added to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack |
These functions return the api object allowing for easy chaining
with the pipe
Handlers can be specified in an annotated route file using one of the method tags followed by the path it pertains to. You can use various tags to describe the handler and these will automatically be converted to OpenAPI documentation. Further, additional tags allow you to modify the behavior of the handler, reflecting the arguments available in the functional approach.
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
function(username) {
find_user_in_db(username)
}
Handlers can be specified in an annotated route file using one of the method tags followed by the path it pertains to. You can use various tags to describe the handler and these will automatically be converted to OpenAPI documentation. Further, additional tags allow you to modify the behavior of the handler, reflecting the arguments available in the functional approach.
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
function(username) {
find_user_in_db(username)
}
You can create async handlers with then chaining using annotation, through
the @async and @then tags
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
#* @async
function(username) {
find_user_in_db(username)
}
#* @then
function(server, response) {
server$log("message", "async operation completed")
response$set_header("etag", "abcdef")
Next
}
You can add authentication using the @auth and @authScope tags
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
#* @auth auth1 || auth2
#* @authScope read, write
#*
function(username) {
find_user_in_db(username)
}
The HTTP specs provide a selection of specific methods that clients can send to the server (your plumber api). While there is no enforcement that the server follows any conventions you should strive to create a server API that adheres to common expectations. It is not required that a server understands all methods, most often the opposite is true. The HTTP methods are described below, but consider consulting MDN to get acquainted with the HTTP spec in general
GET: This method is used to request specific content and is perhaps the
most ubiquitous method in use. GET requests should only retrieve data and
should not contain any body content
HEAD: This method is identical to GET, except the response should only
contain headers, no body. Apart from this it is expected that a HEAD
request is identical to a GET request for the same resource
POST: This method delivers content, in the form of a request body, to the
server, potentially causing a change in the server. In the context of
plumber2 it is often used to call functions that require input larger than
what can be put in the URL
PUT: This method is used to update a specific resource on the server. In
the context of a standard plumber2 server this is rarely relevant, though
usage can come up. PUT is considered by clients to be idempotent meaning
that sending the same PUT request multiple times have no effect
DELETE: This method deletes a resource and is the opposite to PUT. As
with PUT this method has limited use in most standard plumber2 servers
CONNECT: This method request the establishment of a proxy tunnel. It is
considered advanced use and is very unlikely to have a use case for your
plumber2 api
OPTIONS: This method is used by clients to query a server about what
methods and other settings are supported on a server
TRACE: This method is a form of ping that should send a response
containing the request (stripped of any sensitive information). Many
servers disallow this method due to security concerns
PATCH: This method is like PUT but allows partial modification of a
resource
Apart from the above, plumber2 also understands the ANY method which
responds to requests to any of the above methods, assuming that a specific
handler for the method is not found. As the semantics of the various methods
are quite different an ANY handler should mainly be used for rejections or
for setting specific broad headers on the response, not as the main handler
for the request
The path defines the URL the request is being made to with the root removed.
If your plumber2 server runs from http://example.com/api/ and a request is
made to http://example.com/api/user/thomas/, then the path would be
user/thomas/. Paths can be static like the prior example, or dynamic as
described below:
Consider you have a bunch of users. It would be impractical to register a
handler for each one of them. Instead you can use a dynamic path like with
the following syntax: user/<username>/. This path would be matched to any
requests made to user/..something../. The actual value of ..something..
(e.g. thomas) would be made available to the handler (see below). A path
can contain multiple arguments if needed, such as
user/<username>/settings/<setting>/
Apart from path arguments it is also possible to be even less specific by
adding a wildcard to the path. The path user/* will match both
user/thomas/, user/thomas/settings/interests/, and anything other path
that begins with user/. As with arguments a path can contain multiple
wildcards but the use of these have very diminishing returns. Contrary to
path arguments the value(s) corresponding to * is not made available to the
handler.
With the existence of path arguments and wildcards it is possible that
multiple handlers in a route can be matched to a single request. Since only
one can be selected we need to determine which one wins. The priority is
based on the specificity of the path. Consider a server containing the
following handler paths: user/thomas/, user/<username>/,
user/<username>/settings/<setting>/, user/*. These paths will have the
following priority:
user/<username>/settings/<setting>/
user/thomas/
user/<username>/
user/*
The first spot is due to the fact that it is the path with the most elements so it is deemed most specific. For the remaining 3 they all have the same number of elements, but static paths are considered more specific than dynamic paths, and path arguments are considered more specific than wildcards.
A request made to user/carl will thus end up in the third handler, while a
request made to user/thomas will end up in the second. This ordering makes
it possible to both provide default handlers as well as specializations for
specific paths.
The handler is a standard R function that is called when a request is made that matches the handlers path (unless a more specific handler path exists — see above). A handler function can perform any operation a normal R function can do, though you should consider strongly the security implications of your handler functions. However, there are certain expectations in plumber around the arguments a handler function takes and the return value it provides
The handler function can take one or more of the following arguments.
Path arguments: Any path arguments are passed on to the handler. If a
handler is registered for the following path
user/<username>/settings/<setting>/ and it handles a request to
user/thomas/settings/interests/ then it will be called with
username = "thomas", setting = "interest"
request: The request the handler is responding to as a reqres::Request
object
response: The response being returned to the client as a
reqres::Response object
server: The Plumber2 object representing your server implementation
client_id: A string uniquely identifying the session the request comes
from
query: A list giving any additional arguments passed into the handler as
part of the url query string
body: The request body, parsed as specified by the provided parsers
Handlers can return a range of different value types, which will inform plumber2 what to do next:
Next or Break
These two control objects informs plumber2 to either proceed handling the
request (Next) or return the response as is, circumventing any remaining
routes (Break)
NULL or the response objectThis is the same as returning Next, i.e. it signals that handling can
proceed
If you return a ggplot2 object it will get plotted for you (and added to the response assuming a graphics serializer is provided) before handling continues
Any kind of value returned that is not captured by the above description will be set to the response body (overwriting what was already there) and handling is then allowed to continue
Like any function in R, a handler may need to signal that something happened,
either by throwing an error or warning or by emitting a message. You can use
stop(), warning(), and message() as you are used to. For all of them,
the condition message will end up in the log. Further, for stop() any
further handling of the request will end and a 500 Internal Error response
is returned. To take more control over problems you can use the
abort_*() family of conditions from reqres. Like stop()
they will halt any further processing, but they also allow control over what
kind of response is sent back, what kind of information about the issue is
communicated to the client, and what kind of information is logged
internally. The response they send back (except for abort_status()) all
adhere to the HTTP Problem spec defined in
RFC 9457.
While it may feel like a good idea to send a detailed error message back to the client it is often better to only inform the client of what they need to change to solve the issue. Too much information about internal implementation details can be a security risk and forwarding internal errors to a client can help inform the client about how the server has been implemented.
plumber2 supports async handling of requests in one of two ways:
The handler you provide returns a promise object
You set async = TRUE (or the name of a registered async evaluator) when
adding the handler
For 1), there is no more to do. You have full custody over the created
promise and any then()-chaining that might be added to it. For 2) it is a
bit different. In that case you provide a regular function and plumber2 takes
care of converting it to a promise. Due to the nature of promises a handler
being converted to a promise can't take request, response, and server
arguments, so if you need to manipulate these you need to use then (more on
this shortly). The async handler should yield the value that the response
should ultimately get assigned to the body or have plotting side effects (in
which case the plot will get added to the response).
Because you can't manipulate request response, or server in the async
handler it may be needed to add operations to perform once the async handler
has finished. This can be done through the then argument (or using the
@then tag in annotated route files). This takes a list of functions to
chain to the promise using promises::then(). Before the then chain is
executed the response will get the return value of the main handler assigned
to the body. Each then call will receive the same arguments as a standard
request handler as well as result which will hold the return value of the
previous handler in the chain. For the first then call result will be a
boolean signalling if the async handler wants request handling to proceed to
the next route or terminate early. The last call in the chain must return
Next or Break to signal if processing should be allowed to continue to
the next route.
plumber2 supports various authentication schemas which can be added with
api_auth_guard(). An authentication flow for the handler can then be
specified with the auth_flow argument and optional scopes can be set with
the auth_scope argument. The flow is defined by a logical expression
referencing the names of the authenticators part of the flow. Assuming two
authenticators are available, auth1 and auth2, then a flow could be
auth1 && auth2 to require the request passes both authenticators.
Alternatively it could be auth1 || auth2 to require the request passing
either. Flows can be arbitrarily complex with nesting etc, but he OpenAPI
spec has limits to what it can describe so if you want to have an OpenAPI
compliant api you must limit yourself to at most two levels of nesting with
the outer level being || (ie. (auth1 && auth2) || (auth3 && auth4) is ok,
but (auth1 || auth2) && (auth3 || auth4) is not due to the outer level
being &&, and (auth1 && auth2) || (auth3 && (auth4 || auth5)) is not
allowed because it has 3 nesting levels). This is only a limitation of
OpenAPI and plumber2 itself can handle all of the above.
plumber2 also supports requiring specific scopes to access resources. If you require these you must make sure the authenticator provides scopes upon a successful authentication, otherwise the request will be denied.
Other Request Handlers:
api_request_header_handlers
# Standard use api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }) # Specify serializers api() |> api_get( "/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, serializers = get_serializers(c("json", "xml")) ) # Request a download and make it async api() |> api_get( "/the_plot", function() { plot(1:10, 1:10) }, serializers = get_serializers(c("png", "jpeg")), download = TRUE, async = TRUE )# Standard use api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }) # Specify serializers api() |> api_get( "/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, serializers = get_serializers(c("json", "xml")) ) # Request a download and make it async api() |> api_get( "/the_plot", function() { plot(1:10, 1:10) }, serializers = get_serializers(c("png", "jpeg")), download = TRUE, async = TRUE )
These handlers are called before the request body has been received and lets you preemptively reject requests before receiving their full content. If the handler does not return Next then the request will be returned at once. Most of your logic, however, will be in the main handlers and you are asked to consult the api_request_handlers docs for in-depth details on how to use request handlers in general.
api_get_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_head_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_post_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_put_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_delete_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_connect_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_options_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_trace_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_patch_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_any_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL )api_get_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_head_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_post_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_put_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_delete_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_connect_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_options_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_trace_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_patch_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL ) api_any_header( api, path, handler, serializers = get_serializers(), parsers = get_parsers(), use_strict_serializer = FALSE, download = FALSE, async = FALSE, then = NULL, route = NULL )
api |
A plumber2 api object to add the handler to |
path |
A string giving the path the handler responds to. See Details |
handler |
A handler function to call when a request is matched to the path |
serializers |
A named list of serializers that can be used to format the
response before sending it back to the client. Which one is selected is based
on the request |
parsers |
A named list of parsers that can be used to parse the
request body before passing it in as the |
use_strict_serializer |
By default, if a serializer that respects the
requests |
download |
Should the response mark itself for download instead of being
shown inline? Setting this to |
async |
If |
then |
A list of function to be called once the async handler is done.
The functions will be chained using |
route |
The route this handler should be added to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack |
These functions return the api object allowing for easy chaining
with the pipe
Adding request header handler is done in the same way as for standard request handlers. The only difference is that you
include a @header tag as well. It is not normal to document header requests
as they usually exist as internal controls. You can add @noDoc to avoid
generating OpenAPI docs for the handler
#* A header handler authorizing users
#*
#* @get /*
#*
#* @header
#* @noDoc
function(client_id, response) {
if (user_is_allowed(username)) {
Next
} else {
response$status <- 404L
Break
}
}
Other Request Handlers:
api_request_handlers
# Simple size limit (better to use build-in functionality) api() |> api_post_header( "/*", function(request, response) { if (request$get_header("content-type") > 1024) { response$status <- 413L Break } else { Next } } )# Simple size limit (better to use build-in functionality) api() |> api_post_header( "/*", function(request, response) { if (request$get_header("content-type") > 1024) { response$status <- 413L Break } else { Next } } )
This function starts the api with the settings it has defined.
api_run( api, host = NULL, port = NULL, block = !is_interactive(), showcase = is_interactive(), ..., silent = FALSE ) api_stop(api)api_run( api, host = NULL, port = NULL, block = !is_interactive(), showcase = is_interactive(), ..., silent = FALSE ) api_stop(api)
api |
A plumber2 api object to launch or stop |
host, port
|
Host and port to run the api on. If not provided the host and port used during the creation of the Plumber2 api will be used |
block |
Should the console be blocked while running (alternative is
to run in the background). Defaults to |
showcase |
Should the default browser open up at the server address.
If |
... |
Arguments passed on to the |
silent |
Should startup messaging by silenced |
These functions return the api object allowing for easy chaining
with the pipe, even though they will often be the last part of the chain
pa <- api() |> api_get("/", function() { list(msg = "Hello World") }) |> api_on("start", function(...) { cat("I'm alive") }) # Start the server pa |> api_run(block = FALSE) # Stop it again pa |> api_stop()pa <- api() |> api_get("/", function() { list(msg = "Hello World") }) |> api_on("start", function(...) { cat("I'm alive") }) # Start the server pa |> api_run(block = FALSE) # Stop it again pa |> api_stop()
This function adds Cross-Origin Resource Sharing (CORS) to a path in your API. The function can be called multiple times to set up CORS for multiple paths, potentially with different settings for each path. CORS is a complex specification and more can be read about it at the CORS plugin documentation.
api_security_cors( api, path = "/*", origin = "*", methods = c("get", "head", "put", "patch", "post", "delete"), allowed_headers = NULL, exposed_headers = NULL, allow_credentials = FALSE, max_age = NULL )api_security_cors( api, path = "/*", origin = "*", methods = c("get", "head", "put", "patch", "post", "delete"), allowed_headers = NULL, exposed_headers = NULL, allow_credentials = FALSE, max_age = NULL )
api |
A plumber2 api object to add the plugin to |
path |
The path that the policy should apply to. routr path syntax applies, meaning that wilcards and path parameters are allowed. |
origin |
The origin allowed for the path. Can be one of:
|
methods |
The HTTP methods allowed for the |
allowed_headers |
A character vector of request headers allowed when
making the request. If the request contains headers not permitted, then
the response will be blocked by the browser. |
exposed_headers |
A character vector of response headers that should be made available to the client upon a succesful request |
allow_credentials |
A boolean indicating whether credentials are
allowed in the request. Credentials are cookies or HTTP authentication
headers, which are normally stripped from |
max_age |
The duration browsers are allowed to keep the preflight response in the cache |
This functions return the api object allowing for easy chaining
with the pipe
To add CORS to a path you can add @cors <origin> to a
handler annotation. <origin> must be one or more URLs or *, separated by
comma (meaning it is not possible to provide a function using the annotation).
This will add CORS to all endpoints described in the block. The annotation
doesn't allow setting allowed_headers, exposed_headers,
allow_credentials or max_age and the default values will be used.
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
#* @cors https://example.com, https://another-site.com
#*
function(username) {
find_user_in_db(username)
}
Other security features:
api_security_headers(),
api_security_resource_isolation()
# Set up cors for your asset/ path for the https://examples.com origin api() |> api_security_cors( path = "asset/*", origin = "https://examples.com" )# Set up cors for your asset/ path for the https://examples.com origin api() |> api_security_cors( path = "asset/*", origin = "https://examples.com" )
This function adds the SecurityHeaders plugin to your plumber2 API. Please consult the documentation for the plugin for up-to-date information on its behaviour.
api_security_headers( api, content_security_policy = csp(default_src = "self", script_src = "self", script_src_attr = "none", style_src = c("self", "https:", "unsafe-inline"), img_src = c("self", "data:"), font_src = c("self", "https:", "data:"), object_src = "none", base_uri = "self", form_action = "self", frame_ancestors = "self", upgrade_insecure_requests = TRUE), content_security_policy_report_only = NULL, cross_origin_embedder_policy = NULL, cross_origin_opener_policy = "same-origin", cross_origin_resource_policy = "same-origin", origin_agent_cluster = TRUE, referrer_policy = "no-referrer", strict_transport_security = sts(max_age = 63072000, include_sub_domains = TRUE), x_content_type_options = TRUE, x_dns_prefetch_control = FALSE, x_download_options = TRUE, x_frame_options = "SAMEORIGIN", x_permitted_cross_domain_policies = "none", x_xss_protection = FALSE )api_security_headers( api, content_security_policy = csp(default_src = "self", script_src = "self", script_src_attr = "none", style_src = c("self", "https:", "unsafe-inline"), img_src = c("self", "data:"), font_src = c("self", "https:", "data:"), object_src = "none", base_uri = "self", form_action = "self", frame_ancestors = "self", upgrade_insecure_requests = TRUE), content_security_policy_report_only = NULL, cross_origin_embedder_policy = NULL, cross_origin_opener_policy = "same-origin", cross_origin_resource_policy = "same-origin", origin_agent_cluster = TRUE, referrer_policy = "no-referrer", strict_transport_security = sts(max_age = 63072000, include_sub_domains = TRUE), x_content_type_options = TRUE, x_dns_prefetch_control = FALSE, x_download_options = TRUE, x_frame_options = "SAMEORIGIN", x_permitted_cross_domain_policies = "none", x_xss_protection = FALSE )
api |
A plumber2 api object to add the plugin to |
content_security_policy |
Set the value of the |
content_security_policy_report_only |
Set the value of the
|
cross_origin_embedder_policy |
Set the value of the
|
cross_origin_opener_policy |
Set the value of the
|
cross_origin_resource_policy |
Set the value of the
|
origin_agent_cluster |
Set the value of the
|
referrer_policy |
Set the value of the
|
strict_transport_security |
Set the value of the
|
x_content_type_options |
Set the value of the
|
x_dns_prefetch_control |
Set the value of the
|
x_download_options |
Set the value of the
|
x_frame_options |
Set the value of the
|
x_permitted_cross_domain_policies |
Set the value of the
|
x_xss_protection |
Set the value of the
|
This functions return the api object allowing for easy chaining
with the pipe
Security headers doesn't have a dedicated annotation tag, but you can set
it up in a @plumber block
#* @plumber
function(api) {
api |>
api_security_headers()
}
Other security features:
api_security_cors(),
api_security_resource_isolation()
# Add default security headers to an API api() |> api_security_headers()# Add default security headers to an API api() |> api_security_headers()
This function adds resource isolation to a path in your API. The function can be called multiple times to set up resource isolation for multiple paths, potentially with different settings for each path. You can read in depth about resource isolation at the ResourceIsolation plugin documentation.
api_security_resource_isolation( api, path = "/*", allowed_site = "same-site", forbidden_navigation = c("object", "embed"), allow_cors = TRUE )api_security_resource_isolation( api, path = "/*", allowed_site = "same-site", forbidden_navigation = c("object", "embed"), allow_cors = TRUE )
api |
A plumber2 api object to add the plugin to |
path |
The path that the policy should apply to. routr path syntax applies, meaning that wilcards and path parameters are allowed. |
allowed_site |
The allowance level to permit. Either |
forbidden_navigation |
A vector of destinations not allowed for
navigational requests. See the |
allow_cors |
Should |
This functions return the api object allowing for easy chaining
with the pipe
To add resource isolation to a path you can add @rip <allowed_site> to a
handler annotation. This will add resource isolation to all endpoints
described in the block. The annotation doesn't allow setting
forbidden_navigation or allow_cors and the default values will be used.
#* A handler for /user/<username>
#*
#* @param username:string The name of the user to provide information on
#*
#* @get /user/<username>
#*
#* @response 200:{name:string, age:integer, hobbies:[string]} Important
#* information about the user such as their name, age, and hobbies
#*
#* @rip same-origin
#*
function(username) {
find_user_in_db(username)
}
Other security features:
api_security_cors(),
api_security_headers()
# Set up resource isolation for everything inside a user path api() |> api_security_resource_isolation( path = "<user>/*" )# Set up resource isolation for everything inside a user path api() |> api_security_resource_isolation( path = "<user>/*" )
If you need to keep data between requests, but don't want to store it
server-side (see api_datastore()) you can instead pass it back and forth as
an encrypted session cookie. This function sets it up on your api and after
it's use you can now access and set session data in the request and response
$session field. Be aware that session data is send back and forth with all
requests and should thus be kept minimal to avoid congestion on your server.
api_session_cookie( api, key, name = "reqres", expires = NULL, max_age = NULL, path = NULL, secure = NULL, same_site = NULL )api_session_cookie( api, key, name = "reqres", expires = NULL, max_age = NULL, path = NULL, secure = NULL, same_site = NULL )
api |
A plumber2 api object to add the session cookie setup to |
key |
A 32-bit secret key as a hex encoded string or a raw vector to
use for encrypting the session cookie. A valid key can be generated using
|
name |
The name of the cookie |
expires |
A POSIXct object given the expiration time of the cookie |
max_age |
The number of seconds to elapse before the cookie expires |
path |
The URL path this cookie is related to |
secure |
Should the cookie only be send over https |
same_site |
Either |
These functions return the api object allowing for easy chaining
with the pipe
Session cookie setup doesn't have a dedicated annotation tag, but you can set
it up in a @plumber block
#* @plumber
function(api) {
api |>
api_session_cookie(keyring::key_get("my_secret_plumber_key"))
}
key <- reqres::random_key() api() |> api_session_cookie(key, secure = TRUE) |> api_get("/", function(request) { if (isTRUE(request$session$foo)) { msg <- "You've been here before" } else { msg <- "You must be new here" request$session$foo <- TRUE } list( msg = msg ) })key <- reqres::random_key() api() |> api_session_cookie(key, secure = TRUE) |> api_get("/", function(request) { if (isTRUE(request$session$foo)) { msg <- "You've been here before" } else { msg <- "You must be new here" request$session$foo <- TRUE } list( msg = msg ) })
You can serve one or more shiny apps as part of a plumber2 api. The shiny app
launches in a background process and the api will work as a reverse proxy to
forward requests to path to the process and relay the response to the
client. The shiny app is started along with the api and shut down once the
api is stopped. This functionality requires the shiny and callr packages to
be installed. Be aware that all requests to subpaths of path will be
forwarded to the shiny process, and thus not end up in your normal route
api_shiny(api, path, app, except = NULL, auth_flow = NULL, auth_scope = NULL)api_shiny(api, path, app, except = NULL, auth_flow = NULL, auth_scope = NULL)
api |
A plumber2 api to add the shiny app to |
path |
The path to serve the shiny app from |
app |
A shiny app object |
except |
Subpaths to |
auth_flow |
A logical expression giving the authentication flow the client must pass to get access to the resource. |
auth_scope |
The scope requirements of the resource |
This functions return the api object allowing for easy chaining
with the pipe
A shiny app can be served using an annotated route file by using the @shiny
tag and proceeding the annotation block with the shiny app object
#* @shiny /my_app/
shiny::shinyAppDir("./shiny")
blank_shiny <- shiny::shinyApp( ui = shiny::fluidPage(), server = shiny::shinyServer(function(...) {}) ) api() |> api_shiny("my_app/", blank_shiny)blank_shiny <- shiny::shinyApp( ui = shiny::fluidPage(), server = shiny::shinyServer(function(...) {}) ) api() |> api_shiny("my_app/", blank_shiny)
These functions support async request handling. You can register your own as
well using register_async().
mirai_async(...)mirai_async(...)
... |
Further argument passed on to the internal async function. See Details for information on which function handles the formatting internally in each async evaluator |
A function taking expr and envir. The former is the expression to
evaluate and the latter is an environment with additional variables that
should be made available during evaluation
mirai_async() uses mirai::mirai(). It is registered as
"mirai". Be aware that for this evaluator to be performant you should
start up multiple persistent background processes. See mirai::daemons().
# Use the default mirai backend by setting `async = TRUE` with a handler pa <- api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, async = TRUE)# Use the default mirai backend by setting `async = TRUE` with a handler pa <- api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, async = TRUE)
While you can manually create a plumber2 API by calling api(), you will
often need to deploy the api somewhere else. To facilitate this you can
create a _server.yml that encapsulates all of your settings and plumber
files. If you call api() with a path to such a file the API will be
constructed according to its content.
create_server_yml(..., path = ".", constructor = NULL, freeze_opt = TRUE)create_server_yml(..., path = ".", constructor = NULL, freeze_opt = TRUE)
... |
path to files and/or directories that contain annotated plumber files to be used by your API |
path |
The folder to place the generated |
constructor |
The path to a file that creates a plumber2 API object. Can be omitted in which case an API object will be created for you |
freeze_opt |
Logical specifying whether any options you currently have
locally (either as environment variables or R options) should be written to
the |
create_server_yml( "path/to/a/plumber/file.R" )create_server_yml( "path/to/a/plumber/file.R" )
You can provide options for your plumber2 api which will be picked up when
you create the API with api(). Options can be set either through the
internal options() functionality, or by setting environment variables. In
the former case, the name of the option must be prefixed with "plumber2.",
in the latter case the variable name must be in upper case and prefixed with
"PLUMBER2_". If the option is stored as an environment variable then the
value is cast to the type giving in default. See the docs for api() for
the default values of the different options.
get_opts(x, default = NULL) all_opts()get_opts(x, default = NULL) all_opts()
x |
The name of the option |
default |
The default value, if |
For get_opts The value of x, if any, or default. For
all_opts() a named list of all the options that are set
The following options are currently recognized by plumber2. They are all read
at creation time and have a parallel argument in api() where you can also
see their default values. This means that changing an option after
creation/during running will have no effect.
host: The address to serve the server from
port: The port to use for the server
docType: The ui to use for serving OpenAPI documentation
docPath: The path to serve the documentation from
rejectMissingMethods: Should requests to paths that doesn't have a handler for the specific method automatically be rejected with a 405 Method Not Allowed response
ignoreTrailingSlash: Should the trailing slash of a path be ignored when adding handlers and handling requests
maxRequestSize: The maximum allowed size of request bodies
sharedSecret: A shared secret the request must contain to be permitted
compressionLimit: The threshold for response size before automatic compression is used
async: The default async engine to use
# Using `options()` old_opts <- options(plumber2.port = 9889L) get_opts("port") options(old_opts) # Using environment variables old_env <- Sys.getenv("PLUMBER2_PORT") Sys.setenv(PLUMBER2_PORT = 9889) ## If no default is provided the return value is a string get_opts("port") ## Provide a default to hint at the options type get_opts("port", 8080L) Sys.setenv(PLUMBER2_PORT = old_env)# Using `options()` old_opts <- options(plumber2.port = 9889L) get_opts("port") options(old_opts) # Using environment variables old_env <- Sys.getenv("PLUMBER2_PORT") Sys.setenv(PLUMBER2_PORT = 9889) ## If no default is provided the return value is a string get_opts("port") ## Provide a default to hint at the options type get_opts("port", 8080L) Sys.setenv(PLUMBER2_PORT = old_env)
In plumber2 your API can have multiple middleware that a request passes
through. At any point can you short-circuit the remaining middleware by
returning Break, which instructs plumber2 to return the response as is.
Returning Next indicates the opposite, ie that the request should be
allowed to pass on to the next middleware in the chain. A handler function
that doesn't return either of these are assumed to return a value that should
be set to the response body and implicitely continue to the next middleware.
Next Break should_break(x)Next Break should_break(x)
x |
An object to test |
A boolean value
# should_break() only returns TRUE with Break should_break(10) should_break(FALSE) should_break(Next) should_break(Break)# should_break() only returns TRUE with Break should_break(10) should_break(FALSE) should_break(Next) should_break(Break)
These helper functions aid in constructing OpenAPI compliant specifications for your API. The return simple lists and you may thus forego these helpers and instead construct it all manually (or import it from a json or yaml file). The purpose of these helpers is mainly in basic input checking and for documenting the structure. Read more about the spec at https://spec.openapis.org/oas/v3.0.0.html
openapi( openapi = "3.0.0", info = openapi_info(), paths = list(), tags = list() ) openapi_info( title = character(), description = character(), terms_of_service = character(), contact = openapi_contact(), license = openapi_license(), version = character() ) openapi_contact(name = character(), url = character(), email = character()) openapi_license(name = character(), url = character()) openapi_path( summary = character(), description = character(), get = openapi_operation(), put = openapi_operation(), post = openapi_operation(), delete = openapi_operation(), options = openapi_operation(), head = openapi_operation(), patch = openapi_operation(), trace = openapi_operation(), parameters = list() ) openapi_operation( summary = character(), description = character(), operation_id = character(), parameters = list(), request_body = openapi_request_body(), responses = list(), tags = character() ) openapi_parameter( name = character(), location = c("path", "query", "header", "cookie"), description = character(), required = logical(), schema = openapi_schema(), content = openapi_content(), ... ) openapi_header(description = character(), schema = openapi_schema()) openapi_schema(x, default = NULL, min = NULL, max = NULL, ..., required = NULL) openapi_content(...) openapi_request_body( description = character(), content = openapi_content(), required = logical() ) openapi_response( description = character(), content = openapi_content(), headers = list() ) openapi_tag(name = character(), description = character())openapi( openapi = "3.0.0", info = openapi_info(), paths = list(), tags = list() ) openapi_info( title = character(), description = character(), terms_of_service = character(), contact = openapi_contact(), license = openapi_license(), version = character() ) openapi_contact(name = character(), url = character(), email = character()) openapi_license(name = character(), url = character()) openapi_path( summary = character(), description = character(), get = openapi_operation(), put = openapi_operation(), post = openapi_operation(), delete = openapi_operation(), options = openapi_operation(), head = openapi_operation(), patch = openapi_operation(), trace = openapi_operation(), parameters = list() ) openapi_operation( summary = character(), description = character(), operation_id = character(), parameters = list(), request_body = openapi_request_body(), responses = list(), tags = character() ) openapi_parameter( name = character(), location = c("path", "query", "header", "cookie"), description = character(), required = logical(), schema = openapi_schema(), content = openapi_content(), ... ) openapi_header(description = character(), schema = openapi_schema()) openapi_schema(x, default = NULL, min = NULL, max = NULL, ..., required = NULL) openapi_content(...) openapi_request_body( description = character(), content = openapi_content(), required = logical() ) openapi_response( description = character(), content = openapi_content(), headers = list() ) openapi_tag(name = character(), description = character())
openapi |
The OpenAPI version the spec adheres to. The helpers assume 3.0.0 so this is also the default value |
info |
A list as constructed by |
paths |
A named list. The names correspond to endpoints and the elements
are lists as constructed by |
tags |
For |
title |
A string giving the title of the API |
description |
A longer description of the respective element. May use markdown |
terms_of_service |
A URL to the terms of service for the API |
contact |
A list as constructed by |
license |
A list as constructed by |
version |
A string giving the version of the API |
name |
The name of the contact, license, parameter, or tag |
url |
The URL pointing to the contact or license information |
email |
An email address for the contact |
summary |
A one-sentence summary of the path or operation |
get, put, post, delete, options, head, patch, trace
|
A list describing the
specific HTTP method when requested for the path, as constructed by
|
parameters |
A list of parameters that apply to the path and/or
operation. If this is given in |
operation_id |
A unique string that identifies this operation in the API |
request_body |
A list as constructed by |
responses |
A named list with the name corresponding to the response
code and the elements being lists as constructed by |
location |
Where this parameter is coming from. Either |
required |
For |
schema |
A description of the data as constructed by |
content |
A list as constructed by |
... |
Further named arguments to be added to the element. For
|
x |
An R object corresponding to the type of the schema. Supported types are:
|
default |
A default value for the parameter. Must be reconsilable with
the type of |
min, max
|
Bounds for the value of the parameter |
headers |
A named list with names corresponding to headers and elements
as constructed by |
A list
# Create docs for an API with a single endpoint doc <- openapi( info = openapi_info( title = "My awesome api", version = "1.0.0" ), paths = list( "/hello/{name}" = openapi_path( get = openapi_operation( summary = "Get a greeting", parameters = list( openapi_parameter( name = "name", location = "path", description = "Your name", schema = openapi_schema(character()) ) ), responses = list( "200" = openapi_response( description = "a kind message", content = openapi_content( "text/plain" = openapi_schema(character()) ) ) ) ) ) ) ) # Add it to an api api() |> api_doc_add(doc)# Create docs for an API with a single endpoint doc <- openapi( info = openapi_info( title = "My awesome api", version = "1.0.0" ), paths = list( "/hello/{name}" = openapi_path( get = openapi_operation( summary = "Get a greeting", parameters = list( openapi_parameter( name = "name", location = "path", description = "Your name", schema = openapi_schema(character()) ) ), responses = list( "200" = openapi_response( description = "a kind message", content = openapi_content( "text/plain" = openapi_schema(character()) ) ) ) ) ) ) ) # Add it to an api api() |> api_doc_add(doc)
These functions cover a large area of potential request body formats. They are all registered to their standard mime types but users may want to use them to register them to alternative types if they know it makes sense.
parse_csv(...) parse_octet() parse_rds(...) parse_feather(...) parse_parquet(...) parse_text(multiple = FALSE) parse_tsv(...) parse_yaml(...) parse_geojson(...) parse_multipart(parsers = get_parsers())parse_csv(...) parse_octet() parse_rds(...) parse_feather(...) parse_parquet(...) parse_text(multiple = FALSE) parse_tsv(...) parse_yaml(...) parse_geojson(...) parse_multipart(parsers = get_parsers())
... |
Further argument passed on to the internal parsing function. See Details for information on which function handles the parsing internally in each parser |
multiple |
logical: should the conversion be to a single character string or multiple individual characters? |
parsers |
A list of parsers to use for parsing the parts of the body |
A function accepting a raw vector along with a directives argument
that provides further directives from the Content-Type to be passed along
parse_csv() uses readr::read_csv() for parsing. It is registered as
"csv" for the mime types application/csv, application/x-csv,
text/csv, and text/x-csv
parse_multipart uses webutils::parse_multipart() for the initial
parsing. It then goes through each part and tries to find a parser that
matches the content type (either given directly or guessed from the file
extension provided). If a parser is not found it leaves the value as a raw
vector. It is registered as "multi" for the mime type multipart/*
parse_octet() passes the raw data through unchanged. It is registered as
"octet" for the mime type application/octet-stream
parse_rds() uses unserialize() for parsing. It is registered as
"rds" for the mime type application/rds
parse_feather() uses arrow::read_feather() for parsing. It is
registered as "feather" for the mime types
application/vnd.apache.arrow.file and application/feather
parse_parquet() uses arrow::read_parquet() for parsing. It is
registered as "parquet" for the mime type application/vnd.apache.parquet
parse_text() uses rawToChar() for parsing. It is registered as
"text" for the mime types text/plain and text/*
parse_tsv() uses readr::read_tsv() for parsing. It is registered as
"tsv" for the mime types application/tab-separated-values and
text/tab-separated-values
parse_yaml() uses yaml::yaml.load() for parsing. It is registered as
"yaml" for the mime types text/vnd.yaml, application/yaml,
application/x-yaml, text/yaml, and text/x-yaml
parse_geojson() uses geojsonsf::geojson_sf() for parsing. It is
registered as "geojson" for the mime types application/geo+json and
application/vdn.geo+json
reqres::parse_json() is registered as "json" for the mime types
application/json and text/json
reqres::parse_queryform() is registered as "form" for the mime type
application/x-www-form-urlencoded
reqres::parse_xml() is registered as "xml" for the mime types
application/xml and text/xml
reqres::parse_html() is registered as "html" for the mime type
text/html
# You can use parsers directly when adding handlers pa <- api() |> api_post("/hello/<name:string>", function(name, body) { list( msg = paste0("Hello ", name, "!") ) }, parsers = list("text/csv" = parse_csv()))# You can use parsers directly when adding handlers pa <- api() |> api_post("/hello/<name:string>", function(name, body) { list( msg = paste0("Hello ", name, "!") ) }, parsers = list("text/csv" = parse_csv()))
This class encapsulates all of the logic of a plumber2 api, and is what gets
passed around in the functional api of plumber2. The Plumber2 class is a
subclass of the fiery::Fire class. Please consult the documentation for
this for additional information on what this type of server is capable of.
Note that the Plumber2 objects are reference objects, meaning that any
change to it will change all instances of the object.
A new Plumber2-object is initialized using the new() method on the
generator:
api <- Plumber2$new()
|
However, most users will use the functional api of the package and thus
construct one using api()
As Plumber2 objects are using reference semantics new copies of an api cannot
be made simply be assigning it to a new variable. If a true copy of a Plumber2
object is desired, use the clone() method.
fiery::Fire -> Plumber2
request_routerThe router handling requests
header_routerThe router handling partial requests (the request will pass through this router prior to reading in the body)
doc_typeThe type of API documentation to generate. Can be either
"rapidoc" (the default), "redoc", "swagger", or NULL (equating to
not generating API docs)
doc_pathThe URL path to serve the api documentation from
doc_argsFurther arguments to the documentation UI
fiery::Fire$async()fiery::Fire$attach()fiery::Fire$close_ws_con()fiery::Fire$delay()fiery::Fire$exclude_static()fiery::Fire$extinguish()fiery::Fire$get_data()fiery::Fire$has_plugin()fiery::Fire$header()fiery::Fire$is_running()fiery::Fire$log()fiery::Fire$off()fiery::Fire$on()fiery::Fire$reignite()fiery::Fire$remove_async()fiery::Fire$remove_data()fiery::Fire$remove_delay()fiery::Fire$remove_time()fiery::Fire$resume()fiery::Fire$safe_call()fiery::Fire$send()fiery::Fire$serve_static()fiery::Fire$set_client_id_converter()fiery::Fire$set_data()fiery::Fire$set_logger()fiery::Fire$start()fiery::Fire$stop()fiery::Fire$test_header()fiery::Fire$test_message()fiery::Fire$test_request()fiery::Fire$test_websocket()fiery::Fire$time()fiery::Fire$trigger()new()
Create a new Plumber2 api
Plumber2$new(
host = get_opts("host", "127.0.0.1"),
port = get_opts("port", 8080),
doc_type = get_opts("docType", "rapidoc"),
doc_path = get_opts("docPath", "__docs__"),
reject_missing_methods = get_opts("rejectMissingMethods", FALSE),
ignore_trailing_slash = get_opts("ignoreTrailingSlash", TRUE),
max_request_size = get_opts("maxRequestSize"),
shared_secret = get_opts("sharedSecret"),
compression_limit = get_opts("compressionLimit", 1000),
default_async = get_opts("async", "mirai"),
env = caller_env()
)hostA string overriding the default host
portAn port number overriding the default port
doc_typeThe type of API documentation to generate. Can be either
"rapidoc" (the default), "redoc", "swagger", or NULL (equating to
not generating API docs)
doc_pathThe URL path to serve the api documentation from
reject_missing_methodsShould requests to paths that doesn't
have a handler for the specific method automatically be rejected with a
405 Method Not Allowed response with the correct Allow header informing
the client of the implemented methods. Assigning a handler to "any" for
the same path at a later point will overwrite this functionality. Be
aware that setting this to TRUE will prevent the request from falling
through to other routes that might have a matching method and path. This
setting only affects handlers on the request router.
ignore_trailing_slashLogical. Should the trailing slash of a path
be ignored when adding handlers and handling requests. Setting this will
not change the request or the path associated with but just ensure that
both path/to/resource and path/to/resource/ ends up in the same
handler. This setting will only affect routes that are created automatically.
max_request_sizeSets a maximum size of request bodies. Setting this
will add a handler to the header router that automatically rejects requests
based on their Content-Length header
shared_secretAssigns a shared secret to the api. Setting this will
add a handler to the header router that automatically rejects requests if
their Plumber-Shared-Secret header doesn't contain the same value. Be aware
that this type of authentication is very weak. Never put the shared secret in
plain text but rely on e.g. the keyring package for storage. Even so, if
requests are send over HTTP (not HTTPS) then anyone can read the secret and
use it
compression_limitThe size threshold in bytes for trying to compress the response body (it is still dependant on content negotiation)
default_asyncThe default evaluator to use for async request handling
envAn environment that will be used as the default execution environment for the API
A Plumber2 object
format()
Human readable description of the api object
Plumber2$format(...)
...ignored
A character vector
ignite()
Begin running the server. Will trigger the start event
Plumber2$ignite( block = FALSE, showcase = is_interactive(), ..., silent = FALSE )
blockShould the console be blocked while running (alternative is to run in the background)
showcaseShould the default browser open up at the server address.
If TRUE then a browser opens at the root of the api, unless the api
contains OpenAPI documentation in which case it will open at that
location. If a string the string is used as a path to add to the root
before opening.
...Arguments passed on to the start handler
silentShould startup messaging by silenced
add_route()
Add a new route to either the request or header router
Plumber2$add_route(name, route = NULL, header = FALSE, after = NULL, root = "")
nameThe name of the route to add. If a route is already present with this name then the provided route (if any) is merged into it
routeThe route to add. If NULL a new empty route will be
created
headerLogical. Should the route be added to the header router?
afterThe location to place the new route on the stack. NULL
will place it at the end. Will not have an effect if a route with the
given name already exists.
rootThe root path to serve this route from.
request_handler()
Add a handler to a request. See api_request_handlers for detailed information
Plumber2$request_handler( method, path, handler, serializers = NULL, parsers = NULL, use_strict_serializer = FALSE, auth_flow = NULL, auth_scope = NULL, download = FALSE, async = FALSE, then = NULL, doc = NULL, route = NULL, header = FALSE )
methodThe HTTP method to attach the handler to
pathA string giving the path the handler responds to.
handlerA handler function to call when a request is matched to the path
serializersA named list of serializers that can be used to format
the response before sending it back to the client. Which one is selected
is based on the request Accept header
parsersA named list of parsers that can be used to parse the
request body before passing it in as the body argument. Which one is
selected is based on the request Content-Type header
use_strict_serializerBy default, if a serializer that respects
the requests Accept header cannot be found, then the first of the
provided ones are used. Setting this to TRUE will instead send back a
406 Not Acceptable response
auth_flowThe authentication flow the request must be validated by to be allowed into the handler, provided as a logical expression of authenticator names
auth_scopeThe scope required to access this handler given a
successful authentication. Unless your authenticators provide scopes this
should be NULL
downloadShould the response mark itself for download instead of
being shown inline? Setting this to TRUE will set the
Content-Disposition header in the response to attachment. Setting it
to a string is equivalent to setting it to TRUE but will in addition
also set the default filename of the download to the string value
asyncIf FALSE create a regular handler. If TRUE, use the
default async evaluator to create an async handler. If a string, the
async evaluator registered to that name is used. If a function is
provided then this is used as the async evaluator
thenA function to call at the completion of an async handler
docOpenAPI documentation for the handler. Will be added to the
paths$<handler_path>$<handler_method> portion of the API.
routeThe route this handler should be added to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack.
headerLogical. Should the handler be added to the header router
message_handler()
Add a handler to a WebSocket message. See api_message for detailed information
Plumber2$message_handler(handler, async = FALSE, then = NULL)
handlerA function conforming to the specifications laid out in
api_message()
asyncIf FALSE create a regular handler. If TRUE, use the
default async evaluator to create an async handler. If a string, the
async evaluator registered to that name is used. If a function is
provided then this is used as the async evaluator
thenA function to call at the completion of an async handler
redirect()
Add a redirect to the header router. Depending on the value
of permanent it will respond with a 307 Temporary Redirect or 308
Permanent Redirect. from and to can contain path parameters and
wildcards which will be matched between the two to construct the correct
redirect path.
Plumber2$redirect(method, from, to, permanent = TRUE)
methodThe HTTP method the redirect should respond to
fromThe path the redirect should respond to
toThe path/URL to redirect the incoming request towards. After
resolving any path parameters and wildcards it will be used in the
Location header
permanentLogical. Is the redirect considered permanent or temporary? Determines the type of redirect status code to use
parse_file()
Parses a plumber file and updates the app according to it
Plumber2$parse_file(file, env = NULL)
fileThe path to a file to parse
envThe parent environment to the environment the file should be
evaluated in. If NULL the environment provided at construction will be
used
add_api_doc()
Add a (partial) OpenAPI spec to the api docs
Plumber2$add_api_doc(doc, overwrite = FALSE, subset = NULL)
docA list with the OpenAPI documentation
overwriteLogical. Should already existing documentation be
removed or should it be merged together with doc
subsetA character vector giving the path to the subset of the
docs to assign doc to
add_shiny()
Add a shiny app to an api. See api_shiny() for detailed
information
Plumber2$add_shiny( path, app, except = NULL, auth_flow = NULL, auth_scope = NULL )
pathThe path to serve the app from
appA shiny app object
exceptSubpaths to path that should not be forwarded to the
shiny app. Be sure it doesn't contains paths that the shiny app needs
auth_flowThe authentication flow the request must be validated by to be allowed into the handler, provided as a logical expression of authenticator names
auth_scopeThe scope required to access this handler given a
successful authentication. Unless your authenticators provide scopes this
should be NULL
add_report()
Render and serve a Quarto or Rmarkdown document from an
endpoint. See api_report() for more information.
Plumber2$add_report( path, report, ..., doc = NULL, max_age = Inf, async = TRUE, finalize = NULL, continue = FALSE, cache_dir = tempfile(pattern = "plumber2_report"), cache_by_id = FALSE, auth_flow = NULL, auth_scope = NULL, route = NULL )
pathThe base path to serve the report from. Additional endpoints will be created in addition to this.
reportThe path to the report to serve
...Further arguments to quarto::quarto_render() or
rmarkdown::render()
docAn openapi_operation() documentation for the report. Only
query parameters will be used and a request body will be generated from
this for the POST methods.
max_ageThe maximum age in seconds to keep a rendered report before initiating a re-render
asyncShould rendering happen asynchronously (using mirai)
finalizeAn optional function to run before sending the response back. The function will receive the request as the first argument, the response as the second, and the server as the third.
continueA logical that defines whether the response is returned directly after rendering or should be made available to subsequent routes
cache_dirThe location of the render cache. By default a temporary folder is created for it.
cache_by_idShould caching be scoped by the user id. If the rendering is dependent on user-level access to different data this is necessary to avoid data leakage.
auth_flowThe authentication flow the request must be validated by to be allowed into the handler, provided as a logical expression of authenticator names
auth_scopeThe scope required to access this handler given a
successful authentication. Unless your authenticators provide scopes this
should be NULL
routeThe route this handler should be added to. Defaults to the last route in the stack. If the route does not exist it will be created as the last route in the stack.
forward()
Add a reverse proxy from a path to a given URL. See
api_forward() for more details
Plumber2$forward(path, url, except = NULL, auth_flow = NULL, auth_scope = NULL)
pathThe root to forward from
urlThe url to forward to
exceptSubpaths to path that should be exempt from forwarding
auth_flowThe authentication flow the request must be validated by to be allowed into the handler, provided as a logical expression of authenticator names
auth_scopeThe scope required to access this handler given a
successful authentication. Unless your authenticators provide scopes this
should be NULL
add_auth_guard()
Adds an auth guard to your API which can then be referenced in auth flows.
Plumber2$add_auth_guard(guard, name = NULL)
guardAn Guard subclass object defining the scheme
nameThe name to use for referencing the scheme in an auth flow
add_auth()
Add an auth flow to an endpoint
Plumber2$add_auth(method, path, auth_flow, auth_scope = NULL, add_doc = TRUE)
methodThe HTTP method to add auth to
pathA string giving the path to be authenticated
auth_flowA logical expression giving the auth flow the client must pass to get access to the resource
auth_scopeThe scope requirements of the resource
add_docShould OpenAPI documentation be added for the authentication
clone()
The objects of this class are cloneable with this method.
Plumber2$clone(deep = FALSE)
deepWhether to make a deep clone.
plumber supports async request handling in two ways. Either manual by
returning a promise from the handler, or automatic through the @async tag /
async argument in the handler functions. The
default evaluator is controlled by the plumber2.async option or the
PLUMBER2_ASYNC environment variable.
register_async(name, fun, dependency = NULL) show_registered_async() get_async(name = NULL, ...)register_async(name, fun, dependency = NULL) show_registered_async() get_async(name = NULL, ...)
name |
The name of the evaluator |
fun |
A function that, upon calling it returns an evaluator taking an
|
dependency |
Package dependencies for the evaluator. |
... |
Arguments passed on to the async function creator |
# Register an async evaluator based on future (the provided mirai backend is # superior in every way so this is for illustrative purpose) future_async <- function(...) { function(expr, envir) { promises::future_promise( expr = expr, envir = envir, substitute = FALSE, ... ) } } register_async("future", future_async, c("promises", "future"))# Register an async evaluator based on future (the provided mirai backend is # superior in every way so this is for illustrative purpose) future_async <- function(...) { function(expr, envir) { promises::future_promise( expr = expr, envir = envir, substitute = FALSE, ... ) } } register_async("future", future_async, c("promises", "future"))
plumber2 comes with many parsers that should cover almost all standard use cases. Still you might want to provide some of your own, which this function facilitates.
register_parser(name, fun, mime_types, default = TRUE) show_registered_parsers() get_parsers(parsers = NULL)register_parser(name, fun, mime_types, default = TRUE) show_registered_parsers() get_parsers(parsers = NULL)
name |
The name to register the parser function to. If already present the current parser will be overwritten by the one provided by you |
fun |
A function that, when called, returns a binary function that can
parse a request body. The first argument takes a raw vector with the binary
encoding of the request body, the second argument takes any additional
directives given by the requests |
mime_types |
One or more mime types that this parser can handle. The
mime types are allowed to contain wildcards, e.g. |
default |
Should this parser be part of the default set of parsers |
parsers |
Parsers to collect. This can either be a character vector of names of registered parsers or a list. If it is a list then the following expectations apply:
|
If you want to register your own parser, then the function you register must
be a factory function, i.e. a function returning a function. The returned
function must accept two arguments, the first being a raw vector
corresponding to the request body, the second being the parsed directives
from the request Content-Type header. All arguments to the factory function
should be optional.
For get_parsers a named list of parser functions named by their
mime types. The order given in parsers is preserved.
# Register a parser that splits at a character and converts to number register_parser("comma", function(delim = ",") { function(raw, directive) { as.numeric(strsplit(rawToChar(raw), delim)[[1]]) } }, mime_types = "text/plain", default = FALSE)# Register a parser that splits at a character and converts to number register_parser("comma", function(delim = ",") { function(raw, directive) { as.numeric(strsplit(rawToChar(raw), delim)[[1]]) } }, mime_types = "text/plain", default = FALSE)
plumber2 comes with many serializers that should cover almost all standard use cases. Still you might want to provide some of your own, which this function facilitates.
register_serializer(name, fun, mime_type, default = TRUE) show_registered_serializers() get_serializers(serializers = NULL)register_serializer(name, fun, mime_type, default = TRUE) show_registered_serializers() get_serializers(serializers = NULL)
name |
The name to register the serializer function to. If already present the current serializer will be overwritten by the one provided by you |
fun |
A function that, when called, returns a unary function that can
serialize a response body to the mime type defined in |
mime_type |
The format this serializer creates. You should take care to ensure that the value provided is a standard mime type for the format |
default |
Should this serializer be part of the default set of serializers |
serializers |
Serializers to collect. This can either be a character vector of names of registered serializers or a list. If it is a list then the following expectations apply:
|
If you want to register your own serializer, then the function you register must be a factory function, i.e. a function returning a function. The returned function must accept a single argument which is the response body. All arguments to the factory function should be optional.
For get_serializers a named list of serializer functions named by
their mime type. The order given in serializers is preserved.
Using the ... will provide remaining graphics serializers if a
graphics serializer is explicitely requested elsewhere. Otherwise it will
provide the remaining non-graphics serializers. A warning is thrown if a mix
of graphics and non-graphics serializers are requested.
# Add a serializer that deparses the value register_serializer("deparse", function(...) { function(x) { deparse(x, ...) } }, mime_type = "text/plain")# Add a serializer that deparses the value register_serializer("deparse", function(...) { function(x) { deparse(x, ...) } }, mime_type = "text/plain")
These functions cover a large area of potential response body formats. They are all registered to their standard mime type but users may want to use them to register them to alternative types if they know it makes sense.
format_csv(...) format_tsv(...) format_rds(version = "3", ascii = FALSE, ...) format_geojson(...) format_feather(...) format_parquet(...) format_yaml(...) format_htmlwidget(...) format_format(..., sep = "\n") format_print(..., sep = "\n") format_cat(..., sep = "\n") format_unboxed(...) format_png(...) format_jpeg(...) format_tiff(...) format_svg(...) format_bmp(...) format_pdf(...)format_csv(...) format_tsv(...) format_rds(version = "3", ascii = FALSE, ...) format_geojson(...) format_feather(...) format_parquet(...) format_yaml(...) format_htmlwidget(...) format_format(..., sep = "\n") format_print(..., sep = "\n") format_cat(..., sep = "\n") format_unboxed(...) format_png(...) format_jpeg(...) format_tiff(...) format_svg(...) format_bmp(...) format_pdf(...)
... |
Further argument passed on to the internal formatting function. See Details for information on which function handles the formatting internally in each serializer |
version |
the workspace format version to use. |
ascii |
a logical. If |
sep |
The separator between multiple elements |
A function accepting the response body
format_csv() uses readr::format_csv() for formatting. It is registered
as "csv" to the mime type text/csv
format_tsv() uses readr::format_tsv() for formatting. It is registered
as "tsv" to the mime type text/tsv
format_rds() uses serialize() for formatting. It is registered as
"rds" to the mime type application/rds
format_geojson() uses geojsonsf::sfc_geojson() or geojsonsf::sf_geojson()
for formatting depending on the class of the response body. It is
registered as "geojson" to the mime type application/geo+json
format_feather() uses arrow::write_feather() for formatting. It is
registered as "feather" to the mime type
application/vnd.apache.arrow.file
format_parquet() uses nanoparquet::write_parquet() for formatting. It is
registered as "parquet" to the mime type application/vnd.apache.parquet
format_yaml() uses yaml::as.yaml() for formatting. It is registered
as "yaml" to the mime type text/yaml
format_htmlwidget() uses htmlwidgets::saveWidget() for formatting. It is
registered as "htmlwidget" to the mime type text/html
format_format() uses format() for formatting. It is registered
as "format" to the mime type text/plain
format_print() uses print() for formatting. It is registered
as "print" to the mime type text/plain
format_cat() uses cat() for formatting. It is registered
as "cat" to the mime type text/plain
format_unboxed() uses reqres::format_json() with auto_unbox = TRUE for
formatting. It is registered as "unboxedJSON" to the mime type
application/json
reqres::format_json() is registered as "json" to the mime type
application/json
reqres::format_html() is registered as "html" to the mime
type text/html
reqres::format_xml() is registered as "xml" to the mime type
text/xml
reqres::format_plain() is registered as "text" to the mime type
text/plain
Serializing graphic output is special because it requires operations before
and after the handler is executed. Further, handlers creating graphics are
expected to do so through side-effects (i.e. call to graphics rendering) or
by returning a ggplot2 object. If you want to create your own graphics
serializer you should use device_formatter() for constructing it.
format_png() uses ragg::agg_png() for rendering. It is registered
as "png" to the mime type image/png
format_jpeg() uses ragg::agg_jpeg() for rendering. It is registered
as "jpeg" to the mime type image/jpeg
format_tiff() uses ragg::agg_tiff() for rendering. It is registered
as "tiff" to the mime type image/tiff
format_svg() uses svglite::svglite() for rendering. It is registered
as "svg" to the mime type image/svg+xml
format_bmp() uses grDevices::bmp() for rendering. It is registered
as "bmp" to the mime type image/bmp
format_pdf() uses grDevices::pdf() for rendering. It is registered
as "pdf" to the mime type application/pdf
# You can use serializers directly when adding handlers pa <- api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, serializers = list("application/json" = format_unboxed()))# You can use serializers directly when adding handlers pa <- api() |> api_get("/hello/<name:string>", function(name) { list( msg = paste0("Hello ", name, "!") ) }, serializers = list("application/json" = format_unboxed()))