---
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()
```