Skip to contents

Overview

The handler function is responsible for transforming a request (req) into a response (resp).

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:

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.

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.

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.

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'.

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.

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