--- title: "Handler" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Handler} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## Overview The `handler` function is responsible for transforming a request (`req`) into a response (`resp`). ```r library(webqueue) handler <- function (req) { # return (resp) } ``` ## HTTP Request 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: ```r 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()`. ## HTTP Response ### Simple #### A list will be encoded as JSON. ```r 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() ``` #### A character vector will be concatenated together. ```r 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() ``` #### An integer will be interpreted as an HTTP status code. ```r 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() ``` ### Intermediate 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 with `packages = 'webqueue'`. ```r 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() ``` ### Advanced 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. ```r 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() ``` ## Pre/Post Modifications 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. ### Request Parsing 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. * Any modifications to `req` are persistent. * To stop the request from this callback, use `stop()`. * The return value from `parse` is ignored. ```r 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() ``` ### Job Hooks 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. * Any modifications to `job` and `req` are persistent. * To stop the request from this callback, use `job$stop()`. * The return value from hooks are ignored. ```r 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() ``` ### Response Reformatting 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 the `reformat` function - it will > trigger an infinite recursion. Instead, access `job$output`. * To stop the request from this callback, use `job$stop()`. * The return value is used as the new `resp`. ```r reformat <- function (job) { paste0('

', job$output, '

') } wq <- WebQueue$new( handler = ~{ 'Hello' }, reformat = reformat ) RCurl::getURL('http://localhost:8080') #> [1] "

Hello

" wq$stop() ```