The handler
function is responsible for transforming a
request (req
) into a response (resp
).
If the HTTP request is:
POST /dir/file.txt?c=world HTTP/1.1
Host: localhost:8080
User-Agent: httr2/1.1.0 r-curl/6.2.0 libcurl/8.10.1
Accept: */*
Accept-Encoding: deflate, gzip
Cookie: token=abc
x-session-id: 123
Content-Type: application/json
Content-Length: 25
{"a":[1,1,3],"b":"hello"}
Then as.list(req)
will be:
list(
ARGS = list(a = c(1L, 1L, 3L), b = "hello", c = "world"),
COOKIES = list(token = "abc"),
HEADERS = c(
`content-type` = "application/json",
host = "localhost:8080",
`user-agent` = "httr2/1.1.0 r-curl/6.2.0 libcurl/8.10.1",
`x-session-id` = "123" ),
PATH_INFO = "/dir/file.txt",
REMOTE_ADDR = "127.0.0.1",
REQUEST_METHOD = "POST",
SERVER_NAME = "127.0.0.1",
SERVER_PORT = "8080" )
Also good to know:
req
is a bare environment.parent.env(req)
is emptyenv()
.wq <- WebQueue$new(handler = ~{ list(r = 2, d = 2) })
httr2::request('http://localhost:8080') |>
httr2::req_perform() |>
httr2::resp_raw()
#> HTTP/1.1 200 OK
#> Date: Thu, 13 Feb 2025 21:31:44 GMT
#> Content-Type: application/json; charset=utf-8
#> Content-Encoding: gzip
#> Transfer-Encoding: chunked
#>
#> {"r":[2],"d":[2]}
wq$stop()
wq <- WebQueue$new(handler = ~{ LETTERS })
httr2::request('http://localhost:8080') |>
httr2::req_perform() |>
httr2::resp_raw()
#> HTTP/1.1 200 OK
#> Date: Thu, 13 Feb 2025 21:31:44 GMT
#> Content-Type: text/html; charset=utf-8
#> Content-Encoding: gzip
#> Transfer-Encoding: chunked
#>
#> ABCDEFGHIJKLMNOPQRSTUVWXYZ
wq$stop()
wq <- WebQueue$new(handler = ~{ 404L })
httr2::request('http://localhost:8080') |>
httr2::req_error(is_error = function (resp) FALSE) |>
httr2::req_perform() |>
httr2::resp_raw()
#> HTTP/1.1 404 Not Found
#> Date: Thu, 13 Feb 2025 21:31:44 GMT
#> Content-Encoding: gzip
#> Transfer-Encoding: chunked
#>
#> Not Found
wq$stop()
To construct more complex HTTP response, use the
response()
, header()
, cookie()
,
and js_obj()
functions.
Important
These functions will not be in the handler’s environment by default. Either call them with the
webqueue::
prefix, or create a WebQueue withpackages = 'webqueue'
.
wq <- WebQueue$new(
packages = 'webqueue',
handler = ~{
body <- list(data = js_obj(list()))
token <- cookie(token = 'randomstring123')
uid <- header('x-user-id' = 100, expose = TRUE)
response(body, token, uid)
})
httr2::request('http://localhost:8080') |>
httr2::req_perform() |>
httr2::resp_raw()
#> HTTP/1.1 200 OK
#> Date: Thu, 13 Feb 2025 21:31:44 GMT
#> Set-Cookie: token=randomstring123
#> x-user-id: 100
#> Access-Control-Expose-Headers: x-user-id
#> Content-Type: application/json; charset=utf-8
#> Content-Encoding: gzip
#> Transfer-Encoding: chunked
#>
#> {"data":{}}
wq$stop()
To bypass webqueue’s response formatting, wrap your response in
I()
to indicate it should be passed on to httpuv as-is. See
the help page for httpuv::startServer()
for a description
of the expected list(status, headers, body)
object.
Although it says body = NULL
is fine, I have found that to
not be the case.
wq <- WebQueue$new(
handler = ~{
status <- 200L
body <- '{"data":{}}'
headers <- list(
'Set-Cookie' = 'token=randomstring123',
'x-user-id' = '100',
'Access-Control-Expose-Headers' = 'x-user-id',
'Content-Type' = 'application/json; charset=utf-8' )
I(list(body = body, status = status, headers = headers))
})
httr2::request('http://localhost:8080') |>
httr2::req_perform() |>
httr2::resp_raw()
#> HTTP/1.1 200 OK
#> Date: Thu, 13 Feb 2025 21:31:44 GMT
#> Set-Cookie: token=randomstring123
#> x-user-id: 100
#> Access-Control-Expose-Headers: x-user-id
#> Content-Type: application/json; charset=utf-8
#> Content-Encoding: gzip
#> Transfer-Encoding: chunked
#>
#> {"data":{}}
wq$stop()
The handler
function is evaluated on a background
process, and will not have access to any variables on the foreground
process.
However, there are opportunities make modifications on the foreground
process to req
before it is passed to the handler, and to
resp
after it is returned by the handler.
Important
The callbacks here are evaluated on the foreground process. Therefore, ensure they execute quickly so as to not bottleneck request handling.
The parse
function is called on req
(an
environment). Aside from req$ARGS
and
req$COOKIES
, req
is exactly as received from
httpuv. After this callback, extraneous httpuv fields are removed from
req
to minimize the amount of data sent to the background
process.
req
are persistent.stop()
.parse
is ignored.parse <- local({
counter <- 1
function (req) {
req$counter <- counter
counter <<- counter + 1
}
})
wq <- WebQueue$new(
handler = function (req) { req$counter },
parse = parse )
RCurl::getURL('http://localhost:8080')
#> [1] "1"
RCurl::getURL('http://localhost:8080')
#> [1] "2"
RCurl::getURL('http://localhost:8080')
#> [1] "3"
wq$stop()
After parse
is called, the resulting req
is
added to a Job. Callbacks are triggered when the Job enters the
'created'
, 'submitted'
, 'queued'
,
and 'starting'
states.
From these hooks you can edit both the job
and
req
environment objects.
job
and req
are
persistent.job$stop()
.hooks <- list()
# Request received
hooks$created <- function (job) { job$req$ARGS$a <- 1 }
# Submitted to the Queue
hooks$submitted <- function (job) { job$req$ARGS$b <- 2 }
# Accepted by the Queue
hooks$queued <- function (job) { job$req$ARGS$c <- 3 }
# Last chance to edit
hooks$starting <- function (job) { job$req$ARGS$d <- 4 }
wq <- WebQueue$new(
handler = function (req) { req$ARGS },
hooks = hooks )
cat(RCurl::getURL('http://localhost:8080'))
#> {"a":[1],"b":[2],"c":[3],"d":[4]}
wq$stop()
The reformat
function lets you edit resp
immediately after it’s returned by handler
(before webqueue
and httpuv try to interpret it as an HTTP response).
You can access both the job
and req
environments. However, any changes made to req
by
handler
will not be reflected here.
Important
Do NOT call
job$result
from within thereformat
function - it will trigger an infinite recursion. Instead, accessjob$output
.
job$stop()
.resp
.