---
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) {
  # <request processing code>
  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('<h1>', job$output, '</h1>')
}

wq <- WebQueue$new(
  handler  = ~{ 'Hello' },
  reformat = reformat )

RCurl::getURL('http://localhost:8080')
#> [1] "<h1>Hello</h1>"

wq$stop()
```