diff --git a/NAMESPACE b/NAMESPACE index be7f78fed..e6d8bccb9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -23,11 +23,11 @@ export(include_rmd) export(options_plumber) export(parser_csv) export(parser_feather) +export(parser_form) export(parser_json) export(parser_multi) export(parser_none) export(parser_octet) -export(parser_query) export(parser_rds) export(parser_read_file) export(parser_text) diff --git a/NEWS.md b/NEWS.md index 4e093471e..320a1c1ae 100644 --- a/NEWS.md +++ b/NEWS.md @@ -89,7 +89,7 @@ both UIs integration are available from https://github.com/meztez/rapidoc/ and h * Added yaml support, serializer and parser. (@meztez, #556) -* Added parsers: `parser_csv()`, `parser_json()`, `parser_multi()`, `parser_octet()`, `parser_query()`, `parser_rds()`, `parser_text()`, `parser_tsv()`, `parser_yaml()`, `parser_none()`, and pseudo `"all"` (#584) +* Added parsers: `parser_csv()`, `parser_json()`, `parser_multi()`, `parser_octet()`, `parser_form()`, `parser_rds()`, `parser_text()`, `parser_tsv()`, `parser_yaml()`, `parser_none()`, and pseudo `"all"` (#584) * Added `serializer_csv()` (@pachamaltese, #520) @@ -147,6 +147,12 @@ both UIs integration are available from https://github.com/meztez/rapidoc/ and h * Date response header is now supplied by httpuv and not plumber. Fixes non standard date response header issues when using different locales. (@shrektan, #319, #380) +* An error will be thrown if multiple arguments are matched to an Plumber Endpoint route definition. + While it is not required, it is safer to define routes to only use `req` and `res` when there is a possiblity to have multiple arguments match a single parameter name. + Use `req$argsPath`, `req$argsQuery`, and `req$argsPostBody` to access path, query, and postBody parameters respectively. + See `system.file("plumber/17-arguments/plumber.R", package = "plumber")` to view an example with expected output and `plumb_api("plumber", "17-arguments")` to retrieve the api. + (#637) + plumber 0.4.6 -------------------------------------------------------------------------------- diff --git a/R/parse-body.R b/R/parse-body.R index 3203cbdd9..047029c19 100644 --- a/R/parse-body.R +++ b/R/parse-body.R @@ -51,14 +51,14 @@ looks_like_json <- local({ }) parser_picker <- function(content_type, first_byte, filename = NULL, parsers = NULL) { - # parse as a query string + # parse as json or a form if (length(content_type) == 0) { # fast default to json when first byte is 7b (ascii {) if (looks_like_json(first_byte)) { return(parsers$alias$json) } - return(parsers$alias$query) + return(parsers$alias$form) } # remove trailing content type information @@ -87,9 +87,9 @@ parser_picker <- function(content_type, first_byte, filename = NULL, parsers = N return(parsers$regex[[which(fpm)[1]]]) } - # query string + # parse as a form submission if (is.null(filename)) { - return(parsers$alias$query) + return(parsers$alias$form) } # octet @@ -234,7 +234,7 @@ registered_parsers <- function() { # ' make_parser(list(json = list(simplifyVector = FALSE), rds = list())) # ' # ' # default plumber parsers -# ' make_parser(c("json", "query", "text", "octet", "multi")) +# ' make_parser(c("json", "form", "text", "octet", "multi")) make_parser <- function(aliases) { if (inherits(aliases, "plumber_parsed_parsers")) { return(aliases) @@ -316,7 +316,7 @@ make_parser <- function(aliases) { #' non-default behavior. #' #' Parsers are optional. When unspecified, only the [parser_json()], -#' [parser_octet()], [parser_query()] and [parser_text()] are available. +#' [parser_octet()], [parser_form()] and [parser_text()] are available. #' You can use `@parser parser` tag to activate parsers per endpoint. #' Multiple parsers can be activated for the same endpoint using multiple `@parser parser` tags. #' @@ -326,7 +326,7 @@ make_parser <- function(aliases) { #' See [registered_parsers()] for a list of registered parsers. #' #' @param ... parameters supplied to the appropriate internal function -#' @describeIn parsers Query string parser +#' @describeIn parsers Form query string parser #' @examples #' \dontrun{ #' # Overwrite `text/json` parsing behavior to not allow JSON vectors to be simplified @@ -338,7 +338,7 @@ make_parser <- function(aliases) { #' pr$handle("GET", "/upload", function(rds) {rds}, parsers = c("multi", "rds")) #' } #' @export -parser_query <- function() { +parser_form <- function() { parser_text(parseQS) } @@ -502,7 +502,7 @@ register_parsers_onLoad <- function() { register_parser("json", parser_json, fixed = c("application/json", "text/json")) register_parser("multi", parser_multi, fixed = "multipart/form-data") register_parser("octet", parser_octet, fixed = "application/octet-stream") - register_parser("query", parser_query, fixed = "application/x-www-form-urlencoded") + register_parser("form", parser_form, fixed = "application/x-www-form-urlencoded") register_parser("rds", parser_rds, fixed = "application/rds") register_parser("feather", parser_feather, fixed = "application/feather") register_parser("text", parser_text, fixed = "text/plain", regex = "^text/") diff --git a/R/parse-query.R b/R/parse-query.R index 2f6b31efd..b7305142e 100644 --- a/R/parse-query.R +++ b/R/parse-query.R @@ -3,6 +3,7 @@ queryStringFilter <- function(req){ if (is.null(handled) || handled != TRUE) { qs <- req$QUERY_STRING args <- parseQS(qs) + req$argsQuery <- args req$args <- c(req$args, args) req$.internal$queryStringHandled <- TRUE } @@ -17,8 +18,8 @@ parseQS <- function(qs){ } # Looked into using webutils::parse_query() - # Currently not pursuing `parse_query` as it does not handle Encoding issues handled below - # (Combining keys are also not handled by `parse_query`) + # Currently not pursuing `webutils::parse_query` as it does not handle Encoding issues handled below + # (Combining keys are also not handled by `webutils::parse_query`) qs <- stri_replace_first_regex(qs, "^[?]", "") qs <- chartr("+", " ", qs) diff --git a/R/plumber-step.R b/R/plumber-step.R index 4f7278453..cc08f7494 100644 --- a/R/plumber-step.R +++ b/R/plumber-step.R @@ -101,16 +101,16 @@ PlumberStep <- R6Class( ) # @param positional list with names where they were provided. -getRelevantArgs <- function(args, plumberExpression){ - if (length(args) == 0){ +getRelevantArgs <- function(args, plumberExpression) { + if (length(args) == 0) { unnamedArgs <- NULL - } else if (is.null(names(args))){ + } else if (is.null(names(args))) { unnamedArgs <- 1:length(args) } else { unnamedArgs <- which(names(args) == "") } - if (length(unnamedArgs) > 0 ){ + if (length(unnamedArgs) > 0 ) { stop("Can't call a Plumber function with unnammed arguments. Missing names for argument(s) #", paste0(unnamedArgs, collapse=", "), ". Names of argument list was: \"", @@ -120,11 +120,46 @@ getRelevantArgs <- function(args, plumberExpression){ # Extract the names of the arguments this function supports. fargs <- names(formals(eval(plumberExpression))) - if (!"..." %in% fargs){ + if (length(fargs) == 0) { + # no matches + return(list()) + } + + # If only req and res are found in function definition... + # Only call using the first matches of req and res. + # This allows for post body content to have `req` and `res` named arguments and not duplicated values cause issues. + if (all(fargs %in% c("req", "res"))) { + ret <- list() + # using `$` will retrieve the 1st occurance of req,res + # args$req <- req is used within `plumber$route()` + if ("req" %in% fargs) { + ret$req <- args$req + } + if ("res" %in% fargs) { + ret$res <- args$res + } + return(ret) + } + + if (!"..." %in% fargs) { # Use the named arguments that match, drop the rest. args <- args[names(args) %in% fargs] } + # for all args, check if they are duplicated + arg_names <- names(args) + matched_arg_names <- arg_names[arg_names %in% fargs] + duplicated_matched_arg_names <- duplicated(matched_arg_names, fromLast = TRUE) + + if (any(duplicated_matched_arg_names)) { + stop( + "Can't call a Plumber function with duplicated matching formal arguments: ", + paste0(unique(matched_arg_names[duplicated_matched_arg_names]), collapse = ", "), + "\nPlumber recommends that the route's function signature be `function(req, res)`", + "\nand to access arguments via `req$args`, `req$argsPath`, `req$argsPostBody`, or `req$argsQuery`." + ) + } + args } diff --git a/R/plumber.R b/R/plumber.R index 5ea49d758..9fbd60931 100644 --- a/R/plumber.R +++ b/R/plumber.R @@ -150,7 +150,7 @@ plumber <- R6Class( private$maxSize <- getOption('plumber.maxRequestSize', 0) #0 Unlimited self$setSerializer(serializer_json()) # Default parsers to maintain legacy features - self$set_parsers(c("json", "query", "text", "octet", "multi")) + self$set_parsers(c("json", "form", "text", "octet", "multi")) self$setErrorHandler(defaultErrorHandler()) self$set404Handler(default404Handler) self$set_ui() @@ -646,17 +646,20 @@ plumber <- R6Class( return(NULL) } - # Get args out of the query string, + req/res - args <- list() - if (!is.null(req$args)) { - args <- req$args + # These situations should NOT happen as req,res are set in self$call() + # For testing purposes, these checks are added + if (is.null(req$args)) { + req$args <- list(req = req, res = res) + } else { + if (is.null(req$args$req)) { + req$args$req <- req + } + if (is.null(req$args$res)) { + req$args$res <- res + } } - args$res <- res - args$req <- req - req$args <- args path <- req$PATH_INFO - makeHandleStep <- function(name) { function(...) { resetForward() @@ -673,11 +676,21 @@ plumber <- R6Class( } else { private$default_parsers } + req$argsPath <- h$getPathParams(path) + req$argsPostBody <- postbody_parser(req, parsers) + req$args <- c( - h$getPathParams(path), + # req, res + # query string params and any other `req$args` + ## Query string params have been added to `req$args`. + ## At this point, can not include both `req,res` and `req$argsQuery`. So using `req$args` req$args, - postbody_parser(req, parsers) + # path params + req$argsPath, + # post body params + req$argsPostBody ) + return(do.call(h$exec, req$args)) } } @@ -776,11 +789,11 @@ plumber <- R6Class( #' @details required for httpuv interface call = function(req) { # Set the arguments to an empty list - req$args <- list() req$pr <- self req$.internal <- new.env() res <- PlumberResponse$new(private$default_serializer) + req$args <- list(req = req, res = res) # maybe return a promise object self$serve(req, res) @@ -828,7 +841,7 @@ plumber <- R6Class( private$default_serializer <- serializer }, #' @description Sets the default parsers of the router. - #' @details Initialized to `c("json", "query", "text", "octet", "multi")` + #' @details Initialized to `c("json", "form", "text", "octet", "multi")` #' @template pr_set_parsers__parsers set_parsers = function(parsers) { private$default_parsers <- make_parser(parsers) diff --git a/inst/plumber/17-arguments/plumber.R b/inst/plumber/17-arguments/plumber.R new file mode 100644 index 000000000..78696ab69 --- /dev/null +++ b/inst/plumber/17-arguments/plumber.R @@ -0,0 +1,111 @@ +#* Plumber allows for things like URI paths to be accessed via named R function arguments, but this is generally considered bad practice (see the examples below) +#* @serializer print +#* @post /bad-practice// +function(a, b) { + list(a = a, b = b) +} + + + +#* Since URI paths, query params, and post body arguments can have conflicting names, it's better practice to access arguments via the request object +#* @serializer print +#* @post /good-practice// +function(req, res) { + list( + all = req$args, + query = req$argsQuery, + path = req$argsPath, + postBody = req$argsPostBody + ) +} + + +# Test this api... + +### In an R session... +### Run plumber API +# plumb_api("plumber", "17-arguments") %>% print() %>% pr_run(port = 1234) + + +### In a terminal... +### Curl API + +## Fails (conflicting variable `a`) +# curl --data '' '127.0.0.1:1234/bad-practice/1/2?a=3' +# curl --data 'a=3' '127.0.0.1:1234/bad-practice/1/2' +# curl --data 'a=4' '127.0.0.1:1234/bad-practice/1/2?a=3' + +## Works (but missing variable `d`) +# curl --data '' '127.0.0.1:1234/bad-practice/1/2?d=3' +#> $a +#> [1] "1" +#> +#> $b +#> [1] "2" + +## Works (but missing variable `d`) +# curl --data 'd=3' '127.0.0.1:1234/bad-practice/1/2' +#> $a +#> [1] "1" +#> +#> $b +#> [1] "2" + + + +## Safe endpoint setup +# curl --data 'a=5&b=6' '127.0.0.1:1234/good-practice/3/4?a=1&b=2&d=10' +#> $all +#> $all$req +#> +#> +#> $all$res +#> +#> +#> $all$a +#> [1] "1" +#> +#> $all$b +#> [1] "2" +#> +#> $all$d +#> [1] "10" +#> +#> $all$a +#> [1] "3" +#> +#> $all$b +#> [1] "4" +#> +#> $all$a +#> [1] "5" +#> +#> $all$b +#> [1] "6" +#> +#> +#> $query +#> $query$a +#> [1] "1" +#> +#> $query$b +#> [1] "2" +#> +#> $query$d +#> [1] "10" +#> +#> +#> $path +#> $path$a +#> [1] "3" +#> +#> $path$b +#> [1] "4" +#> +#> +#> $postBody +#> $postBody$a +#> [1] "5" +#> +#> $postBody$b +#> [1] "6" diff --git a/man-roxygen/pr_set_parsers__parsers.R b/man-roxygen/pr_set_parsers__parsers.R index 086542c0b..d8f0d7ab8 100644 --- a/man-roxygen/pr_set_parsers__parsers.R +++ b/man-roxygen/pr_set_parsers__parsers.R @@ -21,5 +21,5 @@ #' parsers = list(json = list(simplifyVector = FALSE), rds = list()) #' #' # default plumber parsers -#' parsers = c("json", "query", "text", "octet", "multi") +#' parsers = c("json", "form", "text", "octet", "multi") #' ``` diff --git a/man/PlumberEndpoint.Rd b/man/PlumberEndpoint.Rd index a78745ebc..775c060a3 100644 --- a/man/PlumberEndpoint.Rd +++ b/man/PlumberEndpoint.Rd @@ -165,7 +165,7 @@ parsers = list(json = list()) parsers = list(json = list(simplifyVector = FALSE), rds = list()) # default plumber parsers -parsers = c("json", "query", "text", "octet", "multi") +parsers = c("json", "form", "text", "octet", "multi") }} \item{\code{lines}}{Endpoint block} diff --git a/man/parsers.Rd b/man/parsers.Rd index 8dc0d3413..08d2a54fd 100644 --- a/man/parsers.Rd +++ b/man/parsers.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/parse-body.R -\name{parser_query} -\alias{parser_query} +\name{parser_form} +\alias{parser_form} \alias{parser_json} \alias{parser_text} \alias{parser_yaml} @@ -15,7 +15,7 @@ \alias{parser_none} \title{Plumber Parsers} \usage{ -parser_query() +parser_form() parser_json(...) @@ -54,7 +54,7 @@ non-default behavior. } \details{ Parsers are optional. When unspecified, only the \code{\link[=parser_json]{parser_json()}}, -\code{\link[=parser_octet]{parser_octet()}}, \code{\link[=parser_query]{parser_query()}} and \code{\link[=parser_text]{parser_text()}} are available. +\code{\link[=parser_octet]{parser_octet()}}, \code{\link[=parser_form]{parser_form()}} and \code{\link[=parser_text]{parser_text()}} are available. You can use \verb{@parser parser} tag to activate parsers per endpoint. Multiple parsers can be activated for the same endpoint using multiple \verb{@parser parser} tags. @@ -65,7 +65,7 @@ See \code{\link[=registered_parsers]{registered_parsers()}} for a list of regist } \section{Functions}{ \itemize{ -\item \code{parser_query}: Query string parser +\item \code{parser_form}: Form query string parser \item \code{parser_json}: JSON parser. See \code{\link[jsonlite:read_json]{jsonlite::parse_json()}} for more details. (Defaults to using \code{simplifyVectors = TRUE}) diff --git a/man/plumber.Rd b/man/plumber.Rd index c5e9fe33b..d8289bc50 100644 --- a/man/plumber.Rd +++ b/man/plumber.Rd @@ -629,13 +629,13 @@ parsers = list(json = list()) parsers = list(json = list(simplifyVector = FALSE), rds = list()) # default plumber parsers -parsers = c("json", "query", "text", "octet", "multi") +parsers = c("json", "form", "text", "octet", "multi") }} } \if{html}{\out{}} } \subsection{Details}{ -Initialized to \code{c("json", "query", "text", "octet", "multi")} +Initialized to \code{c("json", "form", "text", "octet", "multi")} } } diff --git a/man/pr_set_parsers.Rd b/man/pr_set_parsers.Rd index a62935d06..19f1739c7 100644 --- a/man/pr_set_parsers.Rd +++ b/man/pr_set_parsers.Rd @@ -30,7 +30,7 @@ parsers = list(json = list()) parsers = list(json = list(simplifyVector = FALSE), rds = list()) # default plumber parsers -parsers = c("json", "query", "text", "octet", "multi") +parsers = c("json", "form", "text", "octet", "multi") }} } \value{ diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index e6176b9a4..9d3a08c98 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -87,7 +87,7 @@ reference: - title: POST Body and Query String Parsers contents: - 'register_parser' - - 'parser_query' + - 'parser_form' - 'getCharacterSet' - title: Response diff --git a/tests/testthat/files/parsers.R b/tests/testthat/files/parsers.R index 003af7d02..19979fdc7 100644 --- a/tests/testthat/files/parsers.R +++ b/tests/testthat/files/parsers.R @@ -14,7 +14,7 @@ return_inputs return_inputs #* @post /mixed -#* @parser query +#* @parser form #* @parser json return_inputs diff --git a/tests/testthat/test-endpoint.R b/tests/testthat/test-endpoint.R index 7711997f3..23930578c 100644 --- a/tests/testthat/test-endpoint.R +++ b/tests/testthat/test-endpoint.R @@ -12,34 +12,65 @@ test_that("Endpoints execute in their environment", { test_that("Missing lines are ok", { expect_silent({ - PlumberEndpoint$new('verb', 'path', { 1 }, environment()) + PlumberEndpoint$new('verb', 'path', { 1 }, new.env(parent = globalenv())) }) }) test_that("Endpoints are exec'able with named arguments.", { foo <- parse(text="foo <- function(x){ x + 1 }") - r <- PlumberEndpoint$new('verb', 'path', foo, environment()) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) expect_equal(r$exec(x=3), 4) }) test_that("Unnamed arguments error", { foo <- parse(text="foo <- function(){ 1 }") - r <- PlumberEndpoint$new('verb', 'path', foo, environment()) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) expect_error(r$exec(3)) foo <- parse(text="foo <- function(x, ...){ x + 1 }") - r <- PlumberEndpoint$new('verb', 'path', foo, environment()) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) expect_error(r$exec(x=1, 3)) }) test_that("Ellipses allow any named args through", { foo <- parse(text="function(...){ sum(unlist(list(...))) }") - r <- PlumberEndpoint$new('verb', 'path', foo, environment()) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) expect_equal(r$exec(a=1, b=2, c=3), 6) - foo <- parse(text="function(...){ list(...) }") - r <- PlumberEndpoint$new('verb', 'path', foo, environment()) - expect_equal(r$exec(a="aa", b="ba"), list(a="aa", b="ba")) + lapply( + c( + "", # with no req or res formals + "req, res, " + ), + function(txt) { + foo <- parse(text=paste0("function(", txt, "...){ list(...) }")) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_equal(r$exec(a="aa", b="ba"), list(a="aa", b="ba")) + expect_equal(r$exec(a="aa1", a="aa2", b = "ba"), list(a="aa1", a="aa2", b = "ba")) + + foo <- parse(text=paste0("function(", txt, "a, ...){ list(a, ...) }")) + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_error(r$exec(a="aa1", a="aa2", b = "ba"), "duplicated matching formal arguments") + } + ) +}) + +test_that("If only req and res are defined, duplicated arguments do not throw an error", { + foo <- parse(text="function(req){ req }") + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_equal(r$exec(req = 1, req = 2, res = 3, res = 4), 1) + + foo <- parse(text="function(res){ res }") + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_equal(r$exec(req = 1, req = 2, res = 3, res = 4), 3) + + foo <- parse(text="function(req, res){ list(req, res) }") + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_equal(r$exec(req = 1, req = 2, res = 3, res = 4), list(1,3)) + + foo <- parse(text="function(req, res, ...){ -1 }") + r <- PlumberEndpoint$new('verb', 'path', foo, new.env(parent = globalenv())) + expect_error(r$exec(req = 1, req = 2, res = 3, res = 4), "duplicated matching formal arguments") }) test_that("Programmatic endpoints work", { diff --git a/tests/testthat/test-parse-body.R b/tests/testthat/test-parse-body.R index 3ac6995d5..8e17cf77c 100644 --- a/tests/testthat/test-parse-body.R +++ b/tests/testthat/test-parse-body.R @@ -7,12 +7,12 @@ test_that("JSON is consumed on POST", { test_that("ending in `==` does not produce a unexpected key", { # See https://github.com/rstudio/plumber/issues/463 - expect_equal(parse_body("randomcharshere==", content_type = NULL, parsers = make_parser("query")), list()) + expect_equal(parse_body("randomcharshere==", content_type = NULL, parsers = make_parser("form")), list()) }) -test_that("Query strings on post are handled correctly", { - expect_equivalent(parse_body("a=", parsers = make_parser("query")), list()) # It's technically a named list() - expect_equal(parse_body("a=1&b=&c&d=1", content_type = NULL, make_parser("query")), list(a="1", d="1")) +test_that("Form query strings on post are handled correctly", { + expect_equivalent(parse_body("a=", parsers = make_parser("form")), list()) # It's technically a named list() + expect_equal(parse_body("a=1&b=&c&d=1", content_type = NULL, make_parser("form")), list(a="1", d="1")) }) test_that("Able to handle UTF-8", { diff --git a/tests/testthat/test-parser.R b/tests/testthat/test-parser.R index 9858b1505..3028dcf14 100644 --- a/tests/testthat/test-parser.R +++ b/tests/testthat/test-parser.R @@ -13,7 +13,7 @@ test_that("parsers can be combined", { expect_parsers("json", "json") - expect_parsers(c("query", "json"), c("query", "json"), sort_items = FALSE) + expect_parsers(c("form", "json"), c("form", "json"), sort_items = FALSE) expect_parsers("all", setdiff(registered_parsers(), c("all", "none"))) expect_parsers(list(all = list()), setdiff(registered_parsers(), c("all", "none"))) @@ -77,6 +77,6 @@ test_that("parsers work", { expect_equal(r$routes$all$parsers, make_parser("all")) expect_equal(r$routes$default$parsers, NULL) expect_equal(r$routes$json$parsers, make_parser("json")) - expect_equal(r$routes$mixed$parsers, make_parser(c("json", "query"))) + expect_equal(r$routes$mixed$parsers, make_parser(c("json", "form"))) expect_equal(r$routes$repeated$parsers, make_parser("json")) }) diff --git a/tests/testthat/test-plumb_api.R b/tests/testthat/test-plumb_api.R index 45330eb02..c5cc1a366 100644 --- a/tests/testthat/test-plumb_api.R +++ b/tests/testthat/test-plumb_api.R @@ -24,22 +24,7 @@ test_that("available_apis() print method works", { expected_apis_output <- c( "Available Plumber APIs:", "* plumber", - " - 01-append", - " - 02-filters", - " - 03-github", - " - 04-mean-sum", - " - 05-static", - " - 06-sessions", - " - 07-mailgun", - " - 08-identity", - " - 09-content-type", - " - 10-welcome", - " - 11-car-inventory", - " - 12-entrypoint", - " - 13-promises", - " - 14-future", - " - 15-openapi-spec", - " - 16-attachment" + paste0(" - ", dir(system.file("plumber", package = "plumber"))) ) expect_equal( diff --git a/vignettes/routing-and-input.Rmd b/vignettes/routing-and-input.Rmd index 89cd27cda..6acd0155b 100644 --- a/vignettes/routing-and-input.Rmd +++ b/vignettes/routing-and-input.Rmd @@ -227,22 +227,28 @@ HTTP requests in Plumber are stored as environments and satisfy the [Rook interf Name | Example | Description ---- | ------- | ----------------------- +`pr` | `plumber::pr()` | The Plumber router that is processing the request `cookies` | `list(cook="abc")` | A list of the cookies as described in [Cookies](#read-cookies) `httpuv.version` | `"1.3.3"` | The version of the underlying [`httpuv` package](https://github.com/rstudio/httpuv) `PATH_INFO` | `"/"` | The path of the incoming HTTP request -`postBody` | `"a=1&b=2"` | The contents of the body of the request. Despite the name, it is available for any HTTP method. +`postBody` | `"a=1&b=2"` | The text contents of the body of the request. Despite the name, it is available for any HTTP method. To disable this parsing, see `?options_plumber`. +`postBodyRaw` | `charToRaw("a=1&b=2")` | The `raw()`, unparsed contents of the body of the request +`args` | `list(a=1,b=2)` | In a route, the combined arguments of `list(req = req, res = res)`, `argsPath`, `argsPostBody`, and `argsQuery`. In a filter, only contains `argsQuery`. +`argsPath` | `list(a=1,b=2)` | The values of the path arguments. +`argsQuery` | `list(a=1,b=2)` | The parsed query string output. +`argsPostBody` | `list(a=1,b=2)` | The parsed post boy output. `QUERY_STRING` | `"?a=123&b=abc"` | The query-string portion of the HTTP request `REMOTE_ADDR` | `"1.2.3.4"` | The IP address of the client making the request `REMOTE_PORT` | `"62108"` | The client port from which the request originated `REQUEST_METHOD` | `"GET"` | The method used for this HTTP request `rook.errors` | N/A | See [Rook docs]( https://github.com/jeffreyhorner/Rook/blob/a5e45f751/README.md#the-input-stream) `rook.input` | N/A | See [Rook docs]( https://github.com/jeffreyhorner/Rook/blob/a5e45f751/README.md#the-error-stream) -`rook.url_scheme` | `"http"` | The "scheme" (typically `http` or `https`). +`rook.url_scheme` | `"http"` | The "scheme" (typically `http` or `https`) `rook.version` | `"1.1-0"` | The version of the rook specification which this environment satisfies `SCRIPT_NAME` | `""` | Unused `SERVER_NAME` | `"127.0.0.1"` | The host portion of the incoming request. You may favor `HTTP_HOST`, if available. `SERVER_PORT` | `"8000"` | The target port for the request -`HTTP_*` | `"HTTP_USER_AGENT"` | Entries for all of the HTTP headers sent with this request. +`HTTP_*` | `"HTTP_USER_AGENT"` | Entries for all of the HTTP headers sent with this request ### Query Strings {#query-strings} @@ -298,7 +304,9 @@ code_chunk(json_serialize(e$exec(req=list(postBody="id=123&name=Jennifer"), id=1 Alternatively, `curl --data '{"id":123, "name": "Jennifer"}' "http://localhost:8000/user"` (formatting the body as JSON) will have the same effect. -As demonstrated above, the raw request body is made available as `req$postBody`. +As demonstrated above, the raw request body is made available as `req$postBody` and `req$postBodyRaw`. + +If multiple parameters are matched to the endpoint formals, an error will be thrown. Due to the nature of how multiple values can be matched to the same argument, it is recommended that `POST` enpoints have a function definition that only accepts the formals `req`, `res`, and `...`. If the endpoint arguments are to be processed like a list, they are available at `req$argsPostBody`, with all arguments at `req$args`. `req$args` is a combination of `list(req = req, res = res)`, `req$argsPath`, `req$argsPostBody`, and `req$argsQuery`. ### Cookies {#read-cookies}