mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
737e5bf420
`hindsight document get <bank> <missing-id>` reported "API endpoint not
found (404)" with endpoint-path/version guidance, while the server had
answered 404 `{"detail":"Document not found"}` — pointing the operator at
an API breakage instead of an absent document.
Two things were dropping the body:
- `api.rs` only ran three of ~70 generated-client calls through
`humanize_client_error`; the rest used a bare `.await?`, so the error
rendered via progenitor's `Display` as a body-less "Unexpected
Response: Response { .. }" and the body was never read at all. Every
call now goes through a `Humanized` extension trait on the call future
(`call(..).humanized().await?`), which attaches status and body.
- `errors.rs` discarded the body in the 404/401/403/5xx branches.
`server_detail` now pulls the JSON `detail` (or the raw body) out of
the error and every branch surfaces it: a 404 that explains itself
prints "Not found (404): Document not found", and only a bodyless 404
— a genuine unknown route — keeps the path/version guidance.
Nothing in the type system stops the next call site from writing the
bare form, and no unit test exercises a real error response, so
`tests/api_error_surface.rs` checks the whole family: every statement in
`api.rs` that awaits a generated-client call must end in `.humanized()`.
75 lines
3.0 KiB
Rust
75 lines
3.0 KiB
Rust
//! Structural guard: every generated-client call must surface the HTTP
|
|
//! response body.
|
|
//!
|
|
//! A bare `self.client.foo(..).await?` converts progenitor's `Error` into
|
|
//! `anyhow::Error` through its `Display`, which for the common
|
|
//! `UnexpectedResponse` case is "Unexpected Response: Response { .. }" — the
|
|
//! body is never read, so the CLI cannot print what the server actually said
|
|
//! and falls back to generic guidance ("API endpoint not found (404)" for a
|
|
//! plain "Document not found"; see issue #4049). `.humanized()` reads the body
|
|
//! into the error instead.
|
|
//!
|
|
//! Nothing in the type system stops the next call site from writing the bare
|
|
//! form — it compiles and only misbehaves against a real error response, which
|
|
//! no unit test exercises. So the family is checked here, over the whole file,
|
|
//! rather than one call at a time.
|
|
|
|
/// Statement boundary for the crude scan below: `api.rs` writes one client call
|
|
/// per statement, so everything since the last `;`/`{`/`}` is the call
|
|
/// expression.
|
|
fn statement_before(source: &str, offset: usize) -> &str {
|
|
let start = [';', '{', '}']
|
|
.iter()
|
|
.filter_map(|delimiter| source[..offset].rfind(*delimiter))
|
|
.max()
|
|
.unwrap_or(0);
|
|
&source[start..offset]
|
|
}
|
|
|
|
#[test]
|
|
fn every_generated_client_call_surfaces_the_response_body() {
|
|
let source = include_str!("../src/api.rs");
|
|
|
|
let mut bare_calls = Vec::new();
|
|
for (offset, _) in source.match_indices(".await?") {
|
|
let statement = statement_before(source, offset);
|
|
let compact: String = statement.chars().filter(|c| !c.is_whitespace()).collect();
|
|
|
|
// `http_client` is the hand-rolled reqwest path (multipart upload,
|
|
// template import); it reads the body itself and formats its own error.
|
|
if !compact.contains("self.client") || compact.contains("http_client") {
|
|
continue;
|
|
}
|
|
if !compact.contains(".humanized()") {
|
|
let line = source[..offset].lines().count();
|
|
let call = statement
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
.trim_start_matches(['{', '}', ';'])
|
|
.trim()
|
|
.to_string();
|
|
bare_calls.push(format!(" src/api.rs:{}: {}", line, call));
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
bare_calls.is_empty(),
|
|
"generated-client calls that swallow the server's response body — \
|
|
end them with `.humanized().await?` instead of `.await?`:\n{}",
|
|
bare_calls.join("\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_guard_notices_a_bare_call() {
|
|
// Guards the guard: a scan that silently matches nothing would pass the
|
|
// test above forever.
|
|
let bare = "let response = self\n.client\n.get_document(bank_id, document_id, None)\n.await?;";
|
|
let statement = statement_before(bare, bare.find(".await?").unwrap());
|
|
let compact: String = statement.chars().filter(|c| !c.is_whitespace()).collect();
|
|
|
|
assert!(compact.contains("self.client"));
|
|
assert!(!compact.contains(".humanized()"));
|
|
}
|