Skip to content

A 200 response with result: 1 but no epidata returns a silent empty tibble — inconsistent with the result: -2 contract #390

Description

@docxology

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions