Summary
While following the envelope handling, I noticed an asymmetry: {"epidata": [], "result": -2, "message": "no results"} is treated as an error/warning path (the contract is even pinned by a test), but a 200 body of {"result": 1, "message": "success"} with no epidata field at all flows straight through: response_content$epidata is NULL, parse_data_frame() produces a 0×0 tibble, and fetch() returns it with no error or warning. A malformed envelope masquerades as a legitimate empty result, so users may conclude "no data exists" when the response was actually broken. I think the fix is to require a non-NULL epidata after the envelope check.
Evidence (dev tip 87b10e1)
R/epidatacall.R:430-432:
# classic: JSON with result/message wrapper
check_epidata_result(response_content, allow_empty = fetch_args$return_empty)
return(response_content$epidata) # NULL when 'epidata' is absent
parse_data_frame() handles it without complaint (R/model.R:268-272): df <- as.data.frame(NULL) → 0×0 tibble. An object-shaped epidata ({...} instead of an array) becomes a 1-row tibble of untyped columns.
Contrast — the -2 contract is pinned in tests/testthat/test-cache.R:111-114:
empty_fixture <- to_httr2_response('{"epidata":[],"result":-2,"message":"no results"}')
...
expect_warning(empty_call <- epidata_call %>% fetch())
Maintainer-runnable repro
Inside a testthat context using the package's existing mock helper:
epidata_call <- pvt_cdc(auth = "k", locations = "ma", epiweeks = epirange(202003, 202304))
resp <- to_httr2_response('{"result":1,"message":"success"}') # no 'epidata'
testthat::local_mocked_bindings(req_perform = function(req, ...) resp, .package = "httr2")
fetch(epidata_call) # returns a 0x0 tibble — no error, no warning
Expected vs. actual
- Expected:
result: 1 without epidata is a malformed envelope → error (or at least a warning with a malformed-envelope class), consistent with how the package treats the equally data-less -2 body.
- Actual: silent 0×0 tibble; downstream code cannot distinguish "legitimately empty" from "broken response".
Suggested fix
After the envelope check, require the field:
if (is.null(response_content$epidata)) {
cli::cli_abort(
"malformed response envelope: 'result' is 1 but 'epidata' is missing",
class = "epidatr__malformed_envelope"
)
}
and verify the value is an array (list/data.frame) before parse_data_frame().
I checked for prior reports (open and closed issues/PRs about empty results — #356/#354/#364 cover cast-API empty handling, not the classic envelope) and found none covering this.
Summary
While following the envelope handling, I noticed an asymmetry:
{"epidata": [], "result": -2, "message": "no results"}is treated as an error/warning path (the contract is even pinned by a test), but a 200 body of{"result": 1, "message": "success"}with noepidatafield at all flows straight through:response_content$epidataisNULL,parse_data_frame()produces a 0×0 tibble, andfetch()returns it with no error or warning. A malformed envelope masquerades as a legitimate empty result, so users may conclude "no data exists" when the response was actually broken. I think the fix is to require a non-NULLepidataafter the envelope check.Evidence (dev tip 87b10e1)
R/epidatacall.R:430-432:parse_data_frame()handles it without complaint (R/model.R:268-272):df <- as.data.frame(NULL)→ 0×0 tibble. An object-shapedepidata({...}instead of an array) becomes a 1-row tibble of untyped columns.Contrast — the
-2contract is pinned intests/testthat/test-cache.R:111-114:Maintainer-runnable repro
Inside a
testthatcontext using the package's existing mock helper:Expected vs. actual
result: 1withoutepidatais a malformed envelope → error (or at least a warning with a malformed-envelope class), consistent with how the package treats the equally data-less-2body.Suggested fix
After the envelope check, require the field:
and verify the value is an array (list/data.frame) before
parse_data_frame().I checked for prior reports (open and closed issues/PRs about empty results — #356/#354/#364 cover cast-API empty handling, not the classic envelope) and found none covering this.