Add a smoke test for real servers, and show why transport errors happen
The suite runs against mock servers written from the upstream source, so
it cannot catch a place where a real server disagrees with that reading.
`examples/smoke_test.rs` can: it lists every resource, checks the
pagination walk against a single oversized request, probes the per_page
cap, and exercises statics, search and health_check. The upload wire
format (POST plus `_method=PUT`) was read from the framework source
rather than observed, so its write tier is the real verification of it.
It prints PASS/FAIL per check and never the contents of a record.
Writes are guarded. `IN_SMOKE_WRITE_HOST` must equal the host in
`IN_BASE_URL`, so a write can only reach a server that was named
explicitly. `IN_BASE_URL` has no default, unlike the other example,
because defaulting to the hosted app is the wrong failure for a tool
that can write. The write tier leaves a gap in the client number
sequence and a soft-deleted row even after cleanup, so it is documented
as disposable-instance only.
Running it against a dead port exposed a diagnosability gap:
`Error::Http` printed "error sending request for url (...)" and nothing
else. reqwest's Display stops at the outermost layer, and the reason
("Connection refused", a DNS failure, a TLS error) lives in `source()`.
The error now renders its whole cause chain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-1
@@ -15,7 +15,11 @@ pub struct ApiErrorBody {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("http transport error: {0}")]
|
||||
/// Rendered with its whole cause chain, because `reqwest`'s own message
|
||||
/// stops at "error sending request for url (...)" and leaves out the part
|
||||
/// that says why: "Connection refused", a DNS failure, a TLS handshake
|
||||
/// error, a timeout.
|
||||
#[error("http transport error: {}", cause_chain(.0))]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
/// The response was not the JSON this endpoint is documented to return.
|
||||
@@ -74,3 +78,23 @@ pub enum Error {
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Joins an error and every error beneath it with `: `.
|
||||
///
|
||||
/// `reqwest::Error`'s `Display` prints only the outermost layer, so the
|
||||
/// reason a request failed lives in `source()` and is otherwise invisible to
|
||||
/// anyone who just prints the error.
|
||||
fn cause_chain(error: &(dyn std::error::Error + 'static)) -> String {
|
||||
let mut message = error.to_string();
|
||||
let mut source = error.source();
|
||||
while let Some(cause) = source {
|
||||
let text = cause.to_string();
|
||||
// Some layers repeat their cause's text; don't print it twice.
|
||||
if !message.contains(&text) {
|
||||
message.push_str(": ");
|
||||
message.push_str(&text);
|
||||
}
|
||||
source = cause.source();
|
||||
}
|
||||
message
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user