9  Request

Throughout the previous chapters, you’ve probably read this over and over again:

The req object is a list which contains everything you need to know about the incoming HTTP request.

And I just made you re-read it again. Because that’s exactly what the req object is.

library(ambiorix)

app <- Ambiorix$new()

app$get("/", function(req, res){
  print(req)
  res$send("check your R console.")
})

app$start()
── A Request
 HEADERS: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, gzip, deflate, br, zstd, en-US,en;q=0.9,
keep-alive, 127.0.0.1:7210, u=0, i, document, navigate, none, ?1, 1, and Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0)
Gecko/20100101 Firefox/147.0
 HTTP_ACCEPT: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
 HTTP_ACCEPT_ENCODING: "gzip, deflate, br, zstd"
 HTTP_ACCEPT_LANGUAGE: "en-US,en;q=0.9"
 HTTP_CACHE_CONTROL:
 HTTP_CONNECTION: "keep-alive"
 HTTP_COOKIE:
 HTTP_HOST: "127.0.0.1:7210"
 HTTP_SEC_FETCH_DEST: "document"
 HTTP_SEC_FETCH_MODE: "navigate"
 HTTP_SEC_FETCH_SITE: "none"
 HTTP_SEC_FETCH_USER: "?1"
 HTTP_UPGRADE_INSECURE_REQUESTS: "1"
 HTTP_USER_AGENT: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0"
 httpuv.version 1.6.16
 PATH_INFO: "/"
 QUERY_STRING: ""
 REMOTE_ADDR: "127.0.0.1"
 REMOTE_PORT: "55520"
 REQUEST_METHOD: "GET"
 SCRIPT_NAME: ""
 SERVER_NAME: "127.0.0.1"
 SERVER_PORT: "127.0.0.1"
 CONTENT_LENGTH:
 CONTENT_TYPE:
 HTTP_REFERER:
 rook.version: "1.1-0"
 rook.url_scheme: "http"

Internally, req is a mutable list built on top of the {httpuv} and {Rook} specifications.

Ambiorix does not abstract it away or “sanitize” it into a new object. What you see is what came off the wire, plus a few conveniences added by Ambiorix.

Here, we shall explore some of its more important bits.

9.1 Query string parameters

req$query is a parsed, named list derived from QUERY_STRING. It contains the URL query parameters, extracted from the URL after the ? character.

9.1.1 Example: Basic Filtering

# URL: /products?category=books&limit=10
app$get("/products", function(req, res) {
  category <- req$query$category %||% "fruits"

  limit <- req$query$limit %||% "20"
  limit <- as.integer(limit)
  
  res$send(paste("Category:", category, "| Limit:", limit))
})

Notes:

  • Values are always strings. Ambiorix does not coerce types for you.
  • Accessing a value which does not exist in req$query returns NULL, which is why we set default values for category and limit.

9.1.2 Handling Duplicate Keys

Unlike some frameworks that collapse duplicates, Ambiorix preserves them. If a query string contains the same key multiple times, req$query will contain multiple entries with that name.

# /filter?tag=r&tag=golang&page=10
req$query
# $tag
# [1] "r"
#
# $tag
# [1] "golang"
#
# $page
# [1] "10"

So in this case, req$query is identical to this:

list(
  tag = "r",
  tag = "golang",
  page = "10"
)

Here’s a reprex on how you might handle such a case:

library(ambiorix)

app <- Ambiorix$new()

app$get("/", function(req, res) {
  res$send("handling duplicate keys")
})

# URL: /filter?tag=r&tag=golang&page=10
app$get("/filter", function(req, res) {
  qp <- req$query
  print(qp)

  tags <- qp[names(qp) == "tag"]
  others <- qp[names(qp) != "tag"]

  out <- list(
    tags = unlist(tags)
  )
  out <- c(out, others)

  res$json(out)
})


app$start(port = 3000L)

9.2 Path parameters

Path parameters are dynamic segments of a URI, defined in your routes with a colon (:). Ambiorix parses these into req$params.

# URL: /users/52/posts/xyz
app$get("/users/:id/posts/:post_id", function(req, res) {
  user_id <- req$params$id

  # path parameters are ALWAYS strings:
  user_id <- as.integer(user_id)

  post_id <- req$params$post_id

  out <- list(
    user_id = user_id,
    post_id = req$params$post_id
  )

  res$json(out)
})

Like query parameters, path parameters are strings. Type conversion is your responsibility.

9.3 Bind variables

Because the req object is a mutable list, you can attach custom data to it. This is usually done in middleware so that those variables are available to the next handlers.

9.3.1 Example: Authentication Middleware

library(ambiorix)

app <- Ambiorix$new()

app$use(function(req, res) {
  # set
  req$user <- list(
    uid = 10L,
    first_name = "John",
    last_name = "Coene",
    status = "active"
  )
})

app$get("/", function(req, res) {
  # get
  out <- list(user = req$user)

  res$json(out)
})

app$start(port = 3000L)

9.3.2 Example: Request Processing Time

library(ambiorix)

app <- Ambiorix$new()

app$get("/", function(req, res) {
  res$send("Request processing time")
})

# middleware to "trace" the request start time
app$use(function(req, res) {
  req$start_time <- Sys.time()
})

app$get("/ping", function(req, res) {
  # pretend this is a time consuming calculation:
  Sys.sleep(3)

  duration <- Sys.time() - req$start_time
  duration <- format(x = duration, format = "%S")

  out <- paste("Request processed in:", duration)

  res$send(out)
})

app$start(port = 3000L)

9.4 Parsers

Ambiorix does not auto-parse request bodies.

There is no magic “body” field in req. You opt in by calling a parser. This avoids accidental parsing, ambiguous content-types, and wasted work.

Ambiorix provides built-in parsers to handle different types of request body data. These parsers make it easy to extract and work with data sent from forms, JSON APIs, and file uploads.

9.4.1 JSON Parser

Use parse_json() to parse JSON data from request bodies:

library(ambiorix)

app <- Ambiorix$new()

app$post("/api/data", function(req, res) {
  data <- parse_json(req)
  print(data)
  res$json(list(received = data))
})

app$start()

9.4.2 Form URL-Encoded Parser

Use parse_form_urlencoded() to parse standard HTML form data:

app$post("/form", function(req, res) {
  form_data <- parse_form_urlencoded(req)
  print(form_data$name) # Access form field by name
  res$send("Form received!")
})

9.4.3 Multipart Form Data Parser

Use parse_multipart() to handle multipart form data, including file uploads:

app$post("/upload", function(req, res) {
  data <- parse_multipart(req)

  # Handle regular form fields
  print(data$username)

  # Handle file uploads
  if ("file" %in% names(data)) {
    file_info <- data$file
    # file_info contains:
    # - value: Raw vector of file contents
    # - content_type: MIME type (e.g., "image/png")
    # - filename: Original filename
    # - name: Form field name

    # Save uploaded file
    temp_path <- tempfile()
    writeBin(file_info$value, temp_path)
  }

  res$send("Upload processed!")
})

9.4.4 Custom Parsers

You can override the default parsers by setting global options:

# Custom JSON parser using jsonlite
my_json_parser <- function(body, ...) {
  txt <- rawToChar(body)
  jsonlite::fromJSON(txt, ...)
}
options(AMBIORIX_JSON_PARSER = my_json_parser)

# Custom multipart parser
options(AMBIORIX_MULTIPART_FORM_DATA_PARSER = my_multipart_parser)

# Custom form URL-encoded parser
options(AMBIORIX_FORM_URLENCODED_PARSER = my_form_parser)

Custom parser functions must accept:

  • body: Raw vector containing the request data
  • content_type: Content-Type header (for multipart parser only)
  • ...: Additional optional parameters