When defining a parser using register_parser, it would be helpful to have access to res, or otherwise have a way to set the response status code.
I find it useful to have the parser check for invalid inputs as well. In this case I want to return 4xx rather than 500 indicating that the failure is on the client side. For example:
library(plumber)
library(jsonlite)
library(jsonvalidate)
validate_inputs <- json_validator("schema.json")
register_parser(
alias = "my_parser",
fixed = "application/json",
parser = function(...) {
function(value, ...) {
value_string <- rawToChar(value)
if (!validate_inputs(value_string)) {
res$status <- 400 # this doesn't work
stop("Request body is not to spec.")
}
return(jsonlite::fromJSON(value))
}
}
)
A workaround is to create a vacuous parser, include req in the handler function signature, add the contents of my_parser to the handler function itself (where res is available), and operate on req$bodyRaw. But this feels like a dirty hack to me.
You should use the router error handler for that. Something like.
validate_inputs <- json_validator("schema.json")
register_parser(
alias = "my_parser",
fixed = "application/json",
parser = function(...) {
parser_text(function(txt_value) {
if (!validate_inputs(txt_value)) {
stop("Request body is not to spec.")
}
parse_json(txt_value, simplifyVector = TRUE, ...)
})
}
)
error_handler <- function(req, res, err){
print(err)
li <- list()
if (err$message == "Request body is not to spec.") {
res$status <- 400
li$error <- "Bad Request"
li$message <- err$message
} else {
res$status <- 500
li$error <- "Internal Server Error"
}
return(li)
}
#* @plumber
function(pr) {
pr$setErrorHandler(error_handler)
}
Any error at the the parser level will be ran through the router error handler.
@jeffkeller87 Do you believe this is still an issue with plumber?
I must admit, I'm not entirely satisfied with the pr_set_error approach for two reasons:
err$message and/or err$call in order to apply status code logic seems clunky.I frequently have different parsers for different handlers, possibly with different status code logic. Capturing all of that in a single error handler seems untenable.
Maybe I'm still missing something, but if the parser argument value to register_parser returned a function that could receive parameters req and res (like handler functions do), I think I could do what I want fairly cleanly. But I understand if there are internal reasons why plumber wouldn't want to include this.
Thank you for your input. Throwing errors from the parser would still invoke the router default error handler as it is. I understand the need to fine tune error handling at a finer level, I don't know how it could be addressed cleanly either.
@jeffkeller87
@meztez is right in that there is no clean way to do this right now.
Building off of @meztez's approach, we can _throw_ an object with classes error and condition to trick R into passing along a list of information. This list of _error_ info can then be post processed inside the error handler. Using this approach, each parser can have its own logic. As long as each parser can return something similar, the error handler does not need to be overly complicated / know of every parser.
register_parser(
alias = "boom",
fixed = "application/json",
parser = function(...) {
parser_text(function(txt_value) {
obj <- list(
# `message` is required for a condition object
message = "custom message",
# Add *anything* you want here
barret_msg = "boom message"
)
# Add classes `error` and `condition` to be able to highjack the error handling
# Add a custom class to be able to quickly handle it differently
class(obj) <- c("barret_custom", "error", "condition", "list")
stop(obj)
})
}
)
```r
error_handler <- function(req, res, err) {
# Check for custom class
if (inherits(err, "barret_custom")) {
# Do custom work here!
# Print structure
str(err)
# Override response serializer
res$serializer = serializer_unboxed_json()
# Return custom value
return(list(
status = 400,
message = paste("Request body is not to spec.", err$barret_msg)
))
}
# Return regular default error handler
return(
plumber:::defaultErrorHandler()(req, res, err)
)
}
```r
pr() %>%
pr_set_error(error_handler) %>%
pr_post("/foo", function() { 42 }, parsers = "boom") %>%
pr_run(port = 8000)
curl --data 'barret-body' '127.0.0.1:8000/foo' -H "Content-Type: application/json"
#> {"status":400,"message":"Request body is not to spec. boom message"}
Output in R session:
#> List of 2
#> $ message : chr "custom message"
#> $ barret_msg: chr "boom message"
#> - attr(*, "class")= chr [1:4] "barret_custom" "error" "condition" "list"
Related: https://adv-r.hadley.nz/conditions.html#signalling in subsection that says
you can create a condition object “by hand” and then pass it to
stop()
@jeffkeller87 Please let me know how this works out for you 😄😄
There will be no clean solution for a while. I hope to address this when we address resolving middleware.
@schloerke defaultErrorHandler is not an exported object from plumber. I'm sorry.
Thanks, @schloerke. The trick to pass along additional info shifts the responsibilities of these functions in a direction that I think is more intuitive, though still not clean, as you mention. In my case, I would send along the desired status code in the synthetic condition object.
I look forward to future work on this front. Please let me know if I can provide more user feedback that would help this development effort. The above solution will definitely work for me in the meantime.
Most helpful comment
Thanks, @schloerke. The trick to pass along additional info shifts the responsibilities of these functions in a direction that I think is more intuitive, though still not clean, as you mention. In my case, I would send along the desired status code in the synthetic condition object.
I look forward to future work on this front. Please let me know if I can provide more user feedback that would help this development effort. The above solution will definitely work for me in the meantime.