I've a toy API with two endrpoints: one that returns a JSON list and one that returns a plot:
#* Plots a time series of the nearest grid cell
#* @param lat
#* @param lon
#* @get /tsplot
#* @png (width = 1920, height = 1080)
function(res, lat = -37, lon = 144) {
}
#* Produces statistics of the nearest grid cell
#* @param lat
#* @param lon
#* @get /stats
function(res, lat = -37, lon = 144) {
}
They both work well for valid input. But I'm trying to make sure I sanitise the inputs (in both cases, lat
and lon
should be numeric and within certain ranges). For the /stats
endpoint, this works great:
if (!is.null(err_msg)) {
res$status <- 400
return(list(error = jsonlite::unbox(err_msg)))
}
But for the /tsplot
endpoint, which uses the @png
serialiser, trying to return the error as JSON results in an error:
Warning in file(data$file, "rb") :
cannot open file '/tmp/Rtmp5bf3eg/file12f072599': No such file or directory
My feeling is that I don't want to necessarily bypass the serialisation entirely鈥擨 just want to switch serialisers when there's an error to handle. I've tried doing it myself, but I get the same error:
if (!is.null(err_msg)) {
res$status <- 400
res$body <- jsonlite::toJSON(list(error = err_msg), unboxed = TRUE)
return(res)
}
How should I approach this?
If you have reached an bad state and you want to return an error message, use stop("My error message")
.
When changing the status of res
and returning a list
, we are manually making a return object that is going through the serializer_json
serializer which essentially returns the object as is. Unfortunately, the png
serializer will try serialize the list object provided, even if it is filled with non png information.
To guarantee it does not get serialized with the _expected_ serializer and instead returns an error, use stop
with an appropriate error message.
I have a preference towards the R package checkmate
for validating inputs. It's really fast and has assert methods built in, great error messages, and helps keep your methods clean while still throwing when a bad input is found.
Example:
a <- 4.5
checkmate::assert_integer(a)
#> Error: Assertion on 'a' failed: Must be of type 'integer', not 'double'.
a <- 20L
checkmate::assert_integer(a, upper = 10)
#> Error: Assertion on 'a' failed: Element 1 is not <= 10.
That sounds great, @schloerke! Is there any way to get the API to return an appropriate HTTP error code, like 400, if I simply throw an error with stop()
?
When I throw an error with stop()
, I'm finding that it makes it to my console output, but it isn't reflected in the API output.
For example:
function(res, lat = -37, lon = 144) {
assert_bound <- partial(assert_numeric, finite = TRUE, any.missing = FALSE,
len = 1)
req_lat <- as.numeric(lat)
req_lon <- as.numeric(lon)
assert_bound(req_lat, lower = -90, upper = 90)
assert_bound(req_lon, lower = -180, upper = 180)
# continue with endpoint functionality
}
If I pass stats?lat=dog
, the console output is:
Warning in (function (res, lat = -37, lon = 144) :
NAs introduced by coercion
But the API output is simply:
{"error":["500 - Internal server error"]}
The same is true if I call stop()
myself.
Historically, the debug
value was set to interactive()
and therefore would not display the message in the response
In plumber v1.0.0
, this can be manually set:
pr() %>%
pr_set_debug(TRUE) %>%
.... # update pr some more
Closing
Most helpful comment
When I throw an error with
stop()
, I'm finding that it makes it to my console output, but it isn't reflected in the API output.For example:
If I pass
stats?lat=dog
, the console output is:But the API output is simply:
The same is true if I call
stop()
myself.