Skip to content

Rework try_as<T> error semantics: surface 4xx/5xx through ec while still reading the body #14

Description

@ashtum

Current behavior

  • try_as<T>() ignores the HTTP status: a 4xx/5xx response completes with ec == success and the converted body.
  • error_for_status().try_as<T>() reports a status error but skips the body entirely, returning a default-constructed T.

Neither form can return both the status error and the body — which is the whole point of the
[ec, r] interface. Error responses often carry useful diagnostics (e.g. RFC 7807 problem
details), and reading them currently requires dropping down to send() plus manual status checks.

Proposed behavior

try_as<T>() always attempts to read and convert the body. ec reports the most significant
error according to this precedence:

  1. transport errors (special case: discarded if they occur after a complete response has been
    parsed, e.g. a rude disconnect)
  2. HTTP protocol errors
  3. HTTP status errors (4xx/5xx)
  4. body conversion errors

The result holds the converted value, or a default-constructed T when conversion fails or the
body is empty. Whether partial data is kept on error remains the per-type policy of
tag_invoke(body_to_tag<T>, ...) (e.g. std::string and file bodies keep the partial result).

For auto [ec, json] = co_await client.get(url).try_as<json::value>(); on a 4xx/5xx response:

scenario ec json
body is valid JSON status error parsed value
body is HTML/plain text (parse fails) status error {}
transport/protocol error while reading body that error {}

On a 2xx response, a conversion error is the first error in the sequence and is reported as-is.

Conversion does not require a matching Content-Type; e.g. try_as<json::value>() attempts to
parse the body regardless of what the server advertises. Many servers mislabel error bodies, and
the conversion error already conveys the mismatch.

Rationale: a status error must not be masked by a conversion error. Error bodies frequently come
from CDN/infrastructure layers as HTML or plain text (e.g. Google APIs returning an HTML page on
429), so a JSON parse error would hide the actual cause of failure.

Remove error_for_status()

With status errors flowing through ec automatically, error_for_status() becomes redundant for
the non-throwing path — any meaningful caller inspects the status anyway. For the throwing path,
as<T>() now treats 4xx/5xx as errors unconditionally; realistically nobody writes
co_await client.post(url).as<T>() expecting a 4xx/5xx to succeed.

  • try_as<T>(): read/convert the body unconditionally, apply the error precedence above
  • as<T>(): throw on 4xx/5xx by default
  • remove error_for_status() from the request builder

(Design settled in a discussion with Peter Dimov.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions