Skip to content

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

@ashtum

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions