| Title: | High-Performance HTTP Server for R via 'Drogon' |
|---|---|
| Description: | Provides an 'R' interface to the 'Drogon' high-performance 'C++' 'HTTP' server framework (<https://github.com/drogonframework/drogon>). Offers a 'plumber'-style application programming interface for building 'REST' services from 'R' with substantially higher throughput. |
| Authors: | Yuri Baramykov [aut, cre] (ORCID: <https://orcid.org/0009-0000-7627-4217>), An Tao [ctb, cph] (Author of the bundled Drogon and Trantor C++ libraries), Shuo Chen [ctb, cph] (Author of the Muduo library, on which Trantor is based), Baptiste Lepilleur [ctb, cph] (Original author of the bundled JsonCpp library), Christopher Dunn [ctb] (Maintainer of JsonCpp), JsonCpp Contributors [ctb, cph] (See src/drogon/third_party/jsoncpp/AUTHORS in the package source), Bert Belder [ctb, cph] (Author of the bundled wepoll library (Windows epoll shim)), mman-win32 contributors [ctb, cph] (Authors of the bundled mman-win32 library; see src/mman-win32/LICENSE) |
| Maintainer: | Yuri Baramykov <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.6 |
| Built: | 2026-05-13 12:12:22 UTC |
| Source: | https://github.com/cran/drogonR |
Creates a fresh, empty 'drogon_app' object that holds the route table and configuration for a server. Routes are added with [dr_get()], [dr_post()], [dr_put()], [dr_delete()], and the server is started with [dr_serve()].
dr_app()dr_app()
The returned object is a mutable [environment] (so route-registration calls modify it in place and return it invisibly for use with '|>').
An object of class 'drogon_app'.
app <- dr_app() app <- dr_get(app, "/", function(req) "hello")app <- dr_app() app <- dr_get(app, "/", function(req) "hello")
Returns the body as raw text, parsed JSON, or a raw byte vector.
dr_body(req, as = c("text", "json", "raw"))dr_body(req, as = c("text", "json", "raw"))
req |
A 'drogon_request'. |
as |
Output form: '"text"' (default), '"json"', or '"raw"'. '"json"' requires the 'jsonlite' package. |
A character string, parsed R object, or raw vector.
Reads 'path' into memory as raw bytes and returns it as the body. For v0.1 the entire file is held in R memory; sendfile-style zero-copy delivery is planned for v0.2 via 'dr_static()'. Files larger than 50 MB emit a warning; files larger than 500 MB raise an error to prevent accidental out-of-memory loads.
dr_file( path, content_type = NULL, status = 200L, headers = list(), download_as = NULL )dr_file( path, content_type = NULL, status = 200L, headers = list(), download_as = NULL )
path |
Path to a regular file readable by the calling process. |
content_type |
MIME type for the response. 'NULL' (the default) auto-detects from the file extension via a built-in table; unknown extensions become 'application/octet-stream'. |
status |
Integer HTTP status code, default 200. |
headers |
Named list of additional response headers. |
download_as |
If a non-empty string, sets 'Content-Disposition: attachment; filename="..."' so browsers prompt to save under that name. |
A response list (see [dr_response()]).
## Not run: dr_file("/tmp/report.pdf") dr_file("/tmp/report.pdf", download_as = "Q3-report.pdf") ## End(Not run)## Not run: dr_file("/tmp/report.pdf") dr_file("/tmp/report.pdf", download_as = "Q3-report.pdf") ## End(Not run)
Looks up a header by name, case-insensitively.
dr_header(req, name)dr_header(req, name)
req |
A 'drogon_request' passed to your route handler. |
name |
Header name (e.g. '"Content-Type"'). |
The header value as a single string, or 'NULL' if absent.
Sets 'Content-Type: text/html; charset=utf-8'.
dr_html(body = "", status = 200L, headers = list())dr_html(body = "", status = 200L, headers = list())
body |
Response body as a character string or raw vector. |
status |
Integer HTTP status code, default 200. |
headers |
Named list of additional response headers. An explicit 'Content-Type' here wins over the default. |
A response list (see [dr_response()]).
dr_html("<h1>hi</h1>")dr_html("<h1>hi</h1>")
Serialises 'x' with [jsonlite::toJSON()] and sets 'Content-Type: application/json' (unless already set in 'headers').
dr_json(x, status = 200L, headers = list(), auto_unbox = TRUE)dr_json(x, status = 200L, headers = list(), auto_unbox = TRUE)
x |
R object to serialise. |
status |
Integer HTTP status code, default 200. |
headers |
Named list of additional response headers. |
auto_unbox |
Passed to [jsonlite::toJSON()]; default 'TRUE' so length-1 vectors become JSON scalars. |
A response list (see [dr_response()]).
dr_json(list(ok = TRUE, n = 1L))dr_json(list(ok = TRUE, n = 1L))
Install a function that builds the response when a route handler or middleware throws an R error. The function receives '(req, err)' — the request object and the captured 'condition' — and must return a response (string, [dr_response()], [dr_json()], etc.). It is called on the main R thread, after the handler / middleware chain has already failed; returning normally short-circuits the default 500.
dr_on_error(app, fn)dr_on_error(app, fn)
app |
A 'drogon_app' created by [dr_app()]. |
fn |
A function of two arguments, 'function(req, err)'. Pass 'NULL' to clear a previously-registered handler. |
If the on-error function itself throws, drogonR logs **both** the original handler error and the on-error error to stderr (via [message()]) and falls back to the default plain-text 500 — the client never sees a hung connection. Only one on-error handler is active per app; calling 'dr_on_error()' again replaces it.
The 'app', invisibly.
app <- dr_app() |> dr_on_error(function(req, err) { dr_json(list(error = conditionMessage(err), path = req$path), status = 500L) }) |> dr_get("/boom", function(req) stop("nope"))app <- dr_app() |> dr_on_error(function(req, err) { dr_json(list(error = conditionMessage(err), path = req$path), status = 500L) }) |> dr_get("/boom", function(req) stop("nope"))
Returns either the named character vector of all query parameters (when 'name = NULL', the default), or the value of a single parameter. Drogon parses and URL-decodes the query string before delivery.
dr_query(req, name = NULL)dr_query(req, name = NULL)
req |
A 'drogon_request'. |
name |
Parameter name, or 'NULL' to get the full named vector. |
A named character vector when 'name' is 'NULL', otherwise a single string or 'NULL' if the parameter is absent.
Adds a rate-limit rule to 'app'. On each matching request, the Drogon I/O thread checks the rule's bucket *before* dispatching to R; if the bucket is empty the request is rejected with HTTP 429 (Too Many Requests) and a 'Retry-After' header. Multiple 'dr_rate_limit()' calls add independent rules — a request must satisfy *all* of them to pass.
dr_rate_limit( app, capacity, window = 60, type = c("sliding_window", "fixed_window", "token_bucket"), scope = c("per_route", "global"), routes = NULL )dr_rate_limit( app, capacity, window = 60, type = c("sliding_window", "fixed_window", "token_bucket"), scope = c("per_route", "global"), routes = NULL )
app |
A 'drogon_app' from [dr_app()]. |
capacity |
Maximum number of requests allowed in 'window' seconds (per-bucket; see 'scope'). Integer '>= 1'. |
window |
Time window for the bucket, in seconds. Default '60'. |
type |
One of '"sliding_window"' (default — counts requests in the trailing 'window' seconds), '"fixed_window"' (resets at wall-clock boundaries), or '"token_bucket"' (constant refill rate with burst capacity). |
scope |
'"per_route"' (default) gives every matched route its own bucket. '"global"' makes one bucket shared across all routes matched by this rule. |
routes |
Either 'NULL' (the default — applies to every registered route) or a character vector of path **prefixes** (e.g. 'c("/api/", "/stream/")'). A route matches if its path starts with any of the given prefixes. |
Per-IP limiting is intentionally not provided: do that in a reverse proxy (nginx, Caddy, Cloudflare). This API is for shaping load on specific endpoints from the application side.
Call 'dr_rate_limit()' *after* registering routes (so prefix matches resolve correctly) and *before* [dr_serve()].
The 'app', invisibly.
## Not run: app <- dr_app() |> dr_get("/health", function(req) "ok") |> dr_get("/api/users", function(req) "users") |> # 100 req/min per route under /api/, health excluded dr_rate_limit(capacity = 100L, window = 60, routes = "/api/") dr_serve(app, port = 8080L) ## End(Not run)## Not run: app <- dr_app() |> dr_get("/health", function(req) "ok") |> dr_get("/api/users", function(req) "users") |> # 100 req/min per route under /api/, health excluded dr_rate_limit(capacity = 100L, window = 60, routes = "/api/") dr_serve(app, port = 8080L) ## End(Not run)
Sets the 'Location' header and an empty body. Default status is 302 (Found / temporary). Use 'status = 301L' for permanent moves, '303L' after a POST, or '307L'/'308L' to preserve the request method.
dr_redirect(location, status = 302L, headers = list())dr_redirect(location, status = 302L, headers = list())
location |
Target URL (absolute or relative). |
status |
Integer HTTP status code, default 302. |
headers |
Named list of additional response headers. |
A response list (see [dr_response()]).
dr_redirect("/login") dr_redirect("https://example.com", status = 301L)dr_redirect("/login") dr_redirect("https://example.com", status = 301L)
Constructs the list shape that route handlers must return: a 'status', a 'body', and a list of headers. Returning the result of 'dr_response()' is interchangeable with returning a plain list with the same fields.
dr_response(body = "", status = 200L, headers = list())dr_response(body = "", status = 200L, headers = list())
body |
Response body as a character string or raw vector. |
status |
Integer HTTP status code, default 200. |
headers |
Named list of response headers. |
A list with elements 'status', 'body', 'headers'.
dr_response("ok") dr_response("not found", status = 404L)dr_response("ok") dr_response("not found", status = 404L)
Register an R function as the handler for a given HTTP method and path. The handler is called for every matching request with a single argument 'req' — a 'drogon_request' object. The handler must return either a single character string (sent as 'text/plain', status 200) or the result of [dr_response()] / [dr_json()].
dr_get(app, path, handler) dr_post(app, path, handler) dr_put(app, path, handler) dr_delete(app, path, handler)dr_get(app, path, handler) dr_post(app, path, handler) dr_put(app, path, handler) dr_delete(app, path, handler)
app |
A 'drogon_app' created by [dr_app()]. |
path |
Request path, e.g. '"/users"'. |
handler |
A function of one argument (the request object). |
Routes must be registered *before* calling [dr_serve()]. Each call returns the 'app' invisibly so calls can be chained with '|>'.
The 'app' (modified in place), invisibly.
app <- dr_app() app <- dr_get(app, "/ping", function(req) "pong") app <- dr_post(app, "/echo", function(req) req$body)app <- dr_app() app <- dr_get(app, "/ping", function(req) "pong") app <- dr_post(app, "/echo", function(req) req$body)
Bind a path to a handler implemented in another R package's C / C++ code, looked up via [base::getNativeSymbolInfo()]-style 'R_RegisterCCallable' / 'R_GetCCallable'. The handler runs on Drogon's worker thread pool — **never** on the R main thread — so its hot path bypasses the R dispatcher entirely. Use this for inference-bound APIs (embeddings, classifiers, GGML/llama.cpp wrappers) where SEXP allocation and 'R_tryEval' per request would dominate latency.
dr_get_cpp(app, path, package, callable) dr_post_cpp(app, path, package, callable) dr_put_cpp(app, path, package, callable) dr_delete_cpp(app, path, package, callable)dr_get_cpp(app, path, package, callable) dr_post_cpp(app, path, package, callable) dr_put_cpp(app, path, package, callable) dr_delete_cpp(app, path, package, callable)
app |
A 'drogon_app' created by [dr_app()]. |
path |
Request path, with the same ':name' / '<name>' / '{name}' placeholder syntaxes as [dr_get()]. Path parameter values are passed positionally to the handler. |
package |
Name of the backend R package that registered the callable. |
callable |
Name passed to the backend's 'R_RegisterCCallable("<package>", "<callable>", ...)' call. |
The handler signature is 'drogonr_unary_handler_t', defined in '<drogonR.h>' (shipped under 'inst/include/'). Backend packages should 'LinkingTo: drogonR' in their DESCRIPTION, '#include <drogonR.h>' in their C / C++ sources, and call 'R_RegisterCCallable("<package>", "<callable>", ...)' in their 'R_init_<package>' to expose the function.
Lookup is eager: 'dr_get_cpp()' calls 'requireNamespace(package)' immediately and resolves '<callable>' against the loaded DLL. If the package is not installed or the callable is unregistered, the error fires here (during route registration), not silently at request time.
The 'app', invisibly.
Native handlers are invoked on Drogon worker threads. They MUST NOT touch any 'SEXP' or call any function in the R API ('Rf_*', 'R_*', 'Rprintf', etc.) — doing so is undefined behaviour. Load models, allocate caches, and read configuration from R BEFORE [dr_serve()] is called; per-request work runs in pure C / C++.
R-side [dr_use()] middleware and the [dr_on_error()] hook are **not** invoked for native routes — they require the request to enter R, which is exactly what this path avoids. Authentication, logging, header injection, etc. must be done either inside the backend handler itself or in front of drogonR (e.g. in a reverse proxy). Per-route [dr_rate_limit()] rules **are** applied (the check runs on the I/O thread before dispatch).
## Not run: # In package ggmlR, R_init_ggmlR() does: # R_RegisterCCallable("ggmlR", "embed", # (DL_FUNC) ggmlr_embed); app <- dr_app() |> dr_post_cpp("/embed", package = "ggmlR", callable = "embed") dr_serve(app, port = 8080L) ## End(Not run)## Not run: # In package ggmlR, R_init_ggmlR() does: # R_RegisterCCallable("ggmlR", "embed", # (DL_FUNC) ggmlr_embed); app <- dr_app() |> dr_post_cpp("/embed", package = "ggmlR", callable = "embed") dr_serve(app, port = 8080L) ## End(Not run)
Like [dr_get_cpp()] but for streaming responses (HTTP chunked / SSE / LLM token streams). The backend handler runs on a drogonR worker thread and pushes chunks via the C callbacks declared in '<drogonR.h>' ('drogonr_stream_handler_t').
dr_get_cpp_stream( app, path, package, callable, content_type = "text/event-stream" ) dr_post_cpp_stream( app, path, package, callable, content_type = "text/event-stream" )dr_get_cpp_stream( app, path, package, callable, content_type = "text/event-stream" ) dr_post_cpp_stream( app, path, package, callable, content_type = "text/event-stream" )
app |
A 'drogon_app' from [dr_app()]. |
path |
URL path; same syntax as [dr_get()]. |
package |
R package that exposes the handler. |
callable |
Symbol name registered via 'R_RegisterCCallable'. |
content_type |
Default 'Content-Type' for the response. Defaults to '"text/event-stream"'. The backend may override this per call by writing to '*out_content_type'. |
The backend is responsible for 'R_RegisterCCallable("<package>", "<callable>", ...)' with a 'drogonr_stream_handler_t' signature. Mismatched signatures are undefined behavior — there is no runtime type check on the function pointer.
'app', invisibly.
R-side [dr_use()] middleware and the [dr_on_error()] hook are **not** invoked for native streaming routes — the request never enters R. Cross-cutting concerns belong in the backend or in a reverse proxy. Per-route [dr_rate_limit()] rules **are** applied on the I/O thread before the worker is dispatched.
Is the drogonR server currently running?
dr_running()dr_running()
'TRUE' if a server is running in this process, 'FALSE' otherwise.
Starts the bundled Drogon HTTP server on the given port and number of I/O threads. The Drogon event loop runs in dedicated C++ threads; incoming requests are dispatched to R handlers on the main R thread via [later::later_fd()].
dr_serve( app, port = 8080L, threads = 1L, workers = 1L, on_worker_start = NULL, max_queue = 1024L, cpp_workers = 4L, upload_path = NULL )dr_serve( app, port = 8080L, threads = 1L, workers = 1L, on_worker_start = NULL, max_queue = 1024L, cpp_workers = 4L, upload_path = NULL )
app |
A 'drogon_app' with at least one registered route. |
port |
TCP port to bind, integer in '1..65535'. Defaults to 8080. |
threads |
Number of Drogon I/O threads per worker, integer '>= 1'. Defaults to 1. |
workers |
Number of OS-level worker processes. '1L' (default) serves in-process. '> 1' spawns workers as fresh 'Rscript' processes and the calling process becomes a thin supervisor. Not supported on Windows. |
on_worker_start |
Optional 'function()' run once per worker before its Drogon listener starts. Use it to load models or open per-worker resources. Errors abort that worker (exit status 1). |
max_queue |
Maximum number of pending requests waiting for an R handler before incoming requests are rejected with HTTP 503 (Service Unavailable). Acts as backpressure when handlers are slower than the arrival rate, preventing unbounded memory growth. 503 responses are sent directly from a Drogon I/O thread without touching R, so overload has no R-side cost. Default '1024L'. |
cpp_workers |
Size of the worker thread pool that runs native (R-bypass) handlers registered via 'dr_*_cpp()' and 'dr_*_cpp_stream()'. Each in-flight cpp request occupies one thread; streaming handlers hold their thread for the full duration of the response. Default '4L'. Increase if you have many concurrent long-running cpp-stream sessions (e.g. LLM token streams). Has no effect on R-side handlers. |
upload_path |
Directory where Drogon stores uploaded files. Defaults to 'NULL', in which case a fresh subdirectory inside [tempdir()] is created so the package never writes to the user's home filespace or the installation directory. Pass an explicit path to override. |
When 'workers > 1', drogonR spawns 'workers' fresh R processes via 'Rscript' (not 'fork()'); each worker runs its own Drogon listener on the same port (Linux/macOS use 'SO_REUSEPORT' for kernel-side load balancing). The calling process is a thin **supervisor** — it does not serve requests itself, only tracks worker pids and reaps them at [dr_stop()]. 'on_worker_start' runs in each worker immediately before its Drogon listener starts, so per-worker state (models, caches) is loaded before the first request lands. Going through 'Rscript'+'exec' (rather than 'parallel::mcparallel()') costs ~200ms of startup per worker but gives each worker a clean R: no inherited sink stack, no inherited 'later' event-loop fds, no half-initialised C++ globals from the supervisor.
If 'on_worker_start' throws in a child, that child exits with status 1 after writing the error to stderr; the supervisor notices it on the next [dr_status()] call and continues with the surviving workers. There is no auto-restart in v0.1.
'NULL', invisibly. Prints a one-line listening message.
Drogon's event loop cannot be restarted in the same R session. After calling [dr_stop()], a new [dr_serve()] in the same process will raise an error — start a fresh R session instead.
## Not run: app <- dr_app() |> dr_get("/hello", function(req) "hi") dr_serve(app, port = 8080L) # Multi-process, each worker loads its own model copy dr_serve(app, port = 8080L, workers = 4L, on_worker_start = function() { model <<- readRDS("model.rds") }) ## End(Not run)## Not run: app <- dr_app() |> dr_get("/hello", function(req) "hi") dr_serve(app, port = 8080L) # Multi-process, each worker loads its own model copy dr_serve(app, port = 8080L, workers = 4L, on_worker_start = function() { model <<- readRDS("model.rds") }) ## End(Not run)
Serve every file under 'dir' at URLs starting with 'mount'. The files are streamed by Drogon directly from a C++ I/O thread (R is never invoked), so this path supports 'Range' requests and auto-detects 'Content-Type'. Both 'GET' and 'HEAD' are accepted; missing files return 404, attempted path traversal returns 403.
dr_static(app, mount, dir)dr_static(app, mount, dir)
app |
A 'drogon_app' created by [dr_app()]. |
mount |
URL prefix to mount under, e.g. '"/assets"'. Must start with '/'. A trailing '/' is stripped. |
dir |
Local directory to serve from. Must exist at [dr_serve()] time. |
The 'app', invisibly.
The handler resolves the requested path against 'dir' and rejects (HTTP 403) any request whose normalised target escapes 'dir' — a '..' segment, an absolute path, or anything else that would otherwise let a remote caller read files outside the mount.
Static files are served entirely from a C++ I/O thread, so R-side [dr_use()] middleware and the [dr_on_error()] hook do **not** apply — they only run for requests that enter R. If you need authentication, custom headers, or per-file logging on assets, put a reverse proxy in front of drogonR or expose the files through a regular [dr_get()] handler instead.
## Not run: app <- dr_app() |> dr_static("/assets", "./public") |> dr_get("/api/ping", function(req) "pong") dr_serve(app, port = 8080L) # GET /assets/logo.png streams ./public/logo.png from C++. ## End(Not run)## Not run: app <- dr_app() |> dr_static("/assets", "./public") |> dr_get("/api/ping", function(req) "pong") dr_serve(app, port = 8080L) # GET /assets/logo.png streams ./public/logo.png from C++. ## End(Not run)
Reports which workers forked by [dr_serve()] are still alive. Polls only when called — there is no background supervisor in v0.1, so dead workers are noticed only here or at [dr_stop()] time. Returns an empty data frame in single-process mode.
dr_status()dr_status()
A data frame with columns 'pid' (integer) and 'alive' (logical), one row per tracked worker child.
Stops the in-process Drogon event loop (when 'workers == 1L') and joins the I/O threads. In supervisor mode ('workers > 1L'), sends 'SIGTERM' to every tracked worker, waits up to ~2s for them to exit, then 'SIGKILL's any survivor. No-op if no server is running and no workers are tracked.
dr_stop()dr_stop()
Drogon cannot be restarted in the same R session — see [dr_serve()].
'NULL', invisibly.
Return value for a route handler when the response should be streamed (HTTP chunked transfer). Instead of producing one body string, the handler returns a 'drogon_stream' describing how to generate chunks on demand. The dispatcher pumps 'next_chunk()' on the main R thread, one chunk per pump, until it signals 'done'.
dr_stream( next_chunk, state = NULL, content_type = "text/event-stream", headers = list(), min_interval = 0 )dr_stream( next_chunk, state = NULL, content_type = "text/event-stream", headers = list(), min_interval = 0 )
next_chunk |
Function 'function(state, cancelled)' returning 'list(chunk = , state = , done = )'. See above. |
state |
Initial state passed to the first pump. Anything an R value can hold; opaque to the dispatcher. |
content_type |
MIME type for the response. Defaults to '"text/event-stream"' since SSE is the most common use case. |
headers |
Named list of additional response headers to send in the initial chunked-transfer response (status is always 200). 'Content-Type' here overrides the 'content_type' argument. |
min_interval |
Minimum delay in seconds between consecutive 'next_chunk()' calls. '0' (default) pumps as fast as the event loop allows. Set to '0.1' to throttle to ~10 chunks/sec, etc. Useful for SSE feeds that should be paced rather than bursted. The delay is a floor, not a guarantee — heavy R-side work or other queued callbacks may push the next pump out further. |
Each pump receives the current 'state' and a 'cancelled' flag. 'cancelled' is 'TRUE' when the dispatcher has detected that the client connection is gone; the generator will be invoked exactly once with 'cancelled = TRUE' so it can free state, and the stream is then closed regardless of what the call returns. It returns a list with three slots:
* 'chunk' — character(1) bytes to send right now (sent verbatim; format SSE / NDJSON / etc. yourself, or use one of the helpers built on top of 'dr_stream()'). * 'state' — the value passed to the next pump. Pass back the incoming ‘state' unchanged if you don’t need to mutate it. * 'done' — 'TRUE' to close the response after this chunk; 'FALSE' to schedule another pump.
A list of class 'drogon_stream' carrying 'next_chunk', 'state', 'content_type', 'headers', and 'min_interval'. Return it from a route handler; the dispatcher recognises the class and opens an HTTP chunked-transfer response, then pumps 'next_chunk()' on the main R thread until it signals 'done = TRUE'.
'next_chunk()' always runs on the main R thread. R is single-threaded, so this is the only place it could safely run. Heavy work inside one pump blocks every other request and every other stream until it returns — keep each step short, and split long generation across many pumps.
## Not run: app <- dr_app() |> dr_get("/sse", function(req) { dr_stream( state = list(i = 0L, n = 5L), next_chunk = function(state, cancelled) { if (cancelled || state$i >= state$n) { return(list(chunk = "", state = state, done = TRUE)) } state$i <- state$i + 1L list(chunk = sprintf("data: %d\n\n", state$i), state = state, done = FALSE) }) }) dr_serve(app, port = 8080L) # curl -N http://127.0.0.1:8080/sse ## End(Not run)## Not run: app <- dr_app() |> dr_get("/sse", function(req) { dr_stream( state = list(i = 0L, n = 5L), next_chunk = function(state, cancelled) { if (cancelled || state$i >= state$n) { return(list(chunk = "", state = state, done = TRUE)) } state$i <- state$i + 1L list(chunk = sprintf("data: %d\n\n", state$i), state = state, done = FALSE) }) }) dr_serve(app, port = 8080L) # curl -N http://127.0.0.1:8080/sse ## End(Not run)
Convenience wrapper around [dr_stream()] for the common case of an SSE feed where each tick emits one 'data:' field. The generator returns 'data' (a string), 'state', and 'done'; the helper formats the SSE frame, splitting embedded newlines into multiple 'data:' lines per the SSE spec, and adds the headers a typical SSE client expects (no caching, no proxy buffering).
dr_stream_sse(generator, state = NULL, headers = list(), min_interval = 0)dr_stream_sse(generator, state = NULL, headers = list(), min_interval = 0)
generator |
Function 'function(state, cancelled)' returning 'list(data = , state = , done = )'. 'data' may contain newlines; they are split into multiple 'data:' lines automatically. An empty 'data' is allowed (sends a keep-alive frame). |
state |
Initial state, as in [dr_stream()]. |
headers |
Extra response headers to merge with the SSE defaults ('Content-Type: text/event-stream', 'Cache-Control: no-cache', 'X-Accel-Buffering: no'). User-supplied entries with the same name win. |
min_interval |
Floor on the delay between consecutive 'generator()' calls, in seconds. See [dr_stream()] for details. Default '0' (no throttling). |
For SSE features beyond plain 'data:' ('event:', 'id:', 'retry:'), use [dr_stream()] directly and format the frame yourself.
A 'drogon_stream' value to return from a route handler.
## Not run: app <- dr_app() |> dr_get("/sse", function(req) { dr_stream_sse( state = list(i = 0L, n = 5L), generator = function(state, cancelled) { if (cancelled || state$i >= state$n) { return(list(data = "", state = state, done = TRUE)) } state$i <- state$i + 1L list(data = sprintf("tick %d", state$i), state = state, done = FALSE) }) }) dr_serve(app, port = 8080L) # curl -N http://127.0.0.1:8080/sse ## End(Not run)## Not run: app <- dr_app() |> dr_get("/sse", function(req) { dr_stream_sse( state = list(i = 0L, n = 5L), generator = function(state, cancelled) { if (cancelled || state$i >= state$n) { return(list(data = "", state = state, done = TRUE)) } state$i <- state$i + 1L list(data = sprintf("tick %d", state$i), state = state, done = FALSE) }) }) dr_serve(app, port = 8080L) # curl -N http://127.0.0.1:8080/sse ## End(Not run)
Sets 'Content-Type: text/plain; charset=utf-8'. The charset is explicit because some intermediaries / older clients otherwise fall back to a non-UTF-8 default and mangle non-ASCII bodies.
dr_text(body = "", status = 200L, headers = list())dr_text(body = "", status = 200L, headers = list())
body |
Response body as a character string or raw vector. |
status |
Integer HTTP status code, default 200. |
headers |
Named list of additional response headers. An explicit 'Content-Type' here wins over the default. |
A response list (see [dr_response()]).
dr_text("hello") dr_text("not found", status = 404L)dr_text("hello") dr_text("not found", status = 404L)
Append a middleware function to the app's middleware chain. Middleware runs in registration order before the matched route handler. Each middleware receives '(req, nxt)': call 'nxt()' to pass control to the next link (its return value is the downstream response, which you may return as-is or modify), or return a response of your own to short- circuit the chain. Throwing an error has the same effect as the route handler throwing — the chain stops and a 500 is returned.
dr_use(app, middleware)dr_use(app, middleware)
app |
A 'drogon_app' created by [dr_app()]. |
middleware |
A function of two arguments, 'function(req, nxt)'. |
The 'app', invisibly.
app <- dr_app() |> dr_use(function(req, nxt) { t0 <- Sys.time() res <- nxt() message("served ", req$path, " in ", format(Sys.time() - t0)) res }) |> dr_get("/ping", function(req) "pong")app <- dr_app() |> dr_use(function(req, nxt) { t0 <- Sys.time() res <- nxt() message("served ", req$path, " in ", format(Sys.time() - t0)) res }) |> dr_get("/ping", function(req) "pong")
Translate a [plumber::pr()] router into [dr_app()] routes and start the drogonR server. The intent is a one-line replacement: existing 'plumber::pr_run(pr)' becomes 'drogonR::pr_run(pr)' without further changes, for the subset of plumber that drogonR can faithfully reproduce.
pr_run(pr, host = "0.0.0.0", port = 8080L, ...)pr_run(pr, host = "0.0.0.0", port = 8080L, ...)
pr |
A 'Plumber' router created by [plumber::pr()]. |
host |
Host to bind. Only '"0.0.0.0"', '"127.0.0.1"', '"localhost"', and '"::"' are accepted; anything else triggers a warning and binds to '0.0.0.0' (drogonR always binds to the wildcard). |
port |
TCP port, integer in '1..65535'. |
... |
Additional arguments forwarded to [dr_serve()] (e.g. 'threads', 'workers', 'max_queue'). Plumber-specific arguments that have no analogue in drogonR ('docs', 'swagger', 'swaggerCallback', 'quiet') are silently accepted and ignored, so that an existing 'plumber::pr_run(pr, docs = FALSE)' call site keeps working after swapping in the shim. |
'NULL', invisibly. Blocks the calling thread until the drogonR server is stopped from another R session via [dr_stop()] (this matches 'plumber::pr_run()' semantics).
* '@get', '@post', '@put', '@delete' annotations. * Path placeholders '<name>' and '<name:type>'. Recognised types are 'int'/'integer', 'dbl'/'double'/'numeric', 'bool'/'logical'; anything else is left as a character. Coercion runs only on path parameters (plumber does not coerce query / body args at the serializer layer either). * Handler arguments resolved by name from path > query > JSON body, with 'req' injected if the handler takes a 'req' parameter. * Default plumber serialisation: every return value goes through 'jsonlite::toJSON(auto_unbox = FALSE)' – including bare strings, which become JSON arrays – for byte-level parity with 'plumber::pr_run()'. Returning a [dr_response()] / [dr_json()] / etc. opts out and is forwarded as-is.
Filters ('@filter'), hooks ('pr_hook()'), mounts ('pr_mount()'), custom parsers/serialisers, OpenAPI assets, websockets, async handlers, and the 'res' (response) parameter of plumber handlers. Each of these triggers an explicit error or per-route warning at 'pr_run()' time so failure is loud, not silent. If you need any of these, use the native [dr_app()] API.