Skip to content

Commit da727e1

Browse files
Merge pull request #36 from floze-the-genius/fix/api-error-display-29
Improve ApiError display output
2 parents 71835bf + 307b990 commit da727e1

4 files changed

Lines changed: 151 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes are recorded here. This project follows semantic versioning,
44
with one pre-1.0 qualification: a minor release may change generated Rust APIs
55
when correcting output that was wrong or incomplete on the wire.
66

7+
## [Unreleased]
8+
9+
### Fixed
10+
11+
- `ApiError` display output now bounds large response-body previews and includes
12+
typed error details or typed-body parse failures when available (#29).
13+
714
## [0.7.0] - 2026-07-17
815

916
### Added

src/generator.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,9 @@ impl CodeGenerator {
976976
/// `headers`, and `body` are always populated so callers can
977977
/// inspect what the server sent without modifying the generated
978978
/// code. `typed` carries the parsed per-operation error variant
979-
/// when the body matched a declared schema.
979+
/// when the body matched a declared schema. Formatting the error
980+
/// limits only the displayed body preview; the public fields
981+
/// retain the complete response and parsing details.
980982
#[derive(Debug, Clone)]
981983
pub struct ApiError<E> {
982984
pub status: u16,
@@ -986,6 +988,21 @@ impl CodeGenerator {
986988
pub parse_error: Option<String>,
987989
}
988990

991+
const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
992+
const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
993+
994+
fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
995+
let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
996+
return std::borrow::Cow::Borrowed(body);
997+
};
998+
999+
let mut displayed =
1000+
String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
1001+
displayed.push_str(&body[..end]);
1002+
displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
1003+
std::borrow::Cow::Owned(displayed)
1004+
}
1005+
9891006
impl<E> ApiError<E> {
9901007
pub fn is_client_error(&self) -> bool {
9911008
(400..500).contains(&self.status)
@@ -1004,7 +1021,22 @@ impl CodeGenerator {
10041021

10051022
impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
10061023
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007-
write!(f, "API error {}: {}", self.status, self.body)
1024+
write!(
1025+
f,
1026+
"API error {}: {}",
1027+
self.status,
1028+
display_api_error_body(&self.body)
1029+
)?;
1030+
1031+
if let Some(typed) = &self.typed {
1032+
write!(f, "; typed: {typed:?}")?;
1033+
}
1034+
1035+
if let Some(parse_error) = &self.parse_error {
1036+
write!(f, "; parse error: {parse_error}")?;
1037+
}
1038+
1039+
Ok(())
10081040
}
10091041
}
10101042

src/http_error.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ pub type HttpResult<T> = Result<T, HttpError>;
212212
/// inspect what the server actually sent without having to hack the generated
213213
/// client. `typed` is `Some(_)` when the raw body was successfully parsed into a
214214
/// per-operation error type; `parse_error` records why parsing failed when not.
215+
/// Formatting the error limits only the displayed body preview; the public
216+
/// fields retain the complete response and parsing details.
215217
#[derive(Debug, Clone)]
216218
pub struct ApiError<E> {
217219
pub status: u16,
@@ -221,6 +223,20 @@ pub struct ApiError<E> {
221223
pub parse_error: Option<String>,
222224
}
223225

226+
const API_ERROR_BODY_DISPLAY_LIMIT: usize = 500;
227+
const API_ERROR_BODY_TRUNCATION_MARKER: &str = "... [truncated]";
228+
229+
fn display_api_error_body(body: &str) -> std::borrow::Cow<'_, str> {
230+
let Some((end, _)) = body.char_indices().nth(API_ERROR_BODY_DISPLAY_LIMIT) else {
231+
return std::borrow::Cow::Borrowed(body);
232+
};
233+
234+
let mut displayed = String::with_capacity(end + API_ERROR_BODY_TRUNCATION_MARKER.len());
235+
displayed.push_str(&body[..end]);
236+
displayed.push_str(API_ERROR_BODY_TRUNCATION_MARKER);
237+
std::borrow::Cow::Owned(displayed)
238+
}
239+
224240
impl<E> ApiError<E> {
225241
pub fn is_client_error(&self) -> bool {
226242
(400..500).contains(&self.status)
@@ -233,7 +249,22 @@ impl<E> ApiError<E> {
233249

234250
impl<E: std::fmt::Debug> std::fmt::Display for ApiError<E> {
235251
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236-
write!(f, "API error {}: {}", self.status, self.body)
252+
write!(
253+
f,
254+
"API error {}: {}",
255+
self.status,
256+
display_api_error_body(&self.body)
257+
)?;
258+
259+
if let Some(typed) = &self.typed {
260+
write!(f, "; typed: {typed:?}")?;
261+
}
262+
263+
if let Some(parse_error) = &self.parse_error {
264+
write!(f, "; parse error: {parse_error}")?;
265+
}
266+
267+
Ok(())
237268
}
238269
}
239270

tests/http_error_test.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,65 @@
1-
use openapi_to_rust::http_error::{HttpError, HttpResult};
1+
use openapi_to_rust::http_error::{ApiError, HttpError, HttpResult};
2+
use reqwest::header::HeaderMap;
3+
4+
#[derive(Debug)]
5+
enum TypedApiError {
6+
Invalid,
7+
}
8+
9+
fn api_error<E>(
10+
body: impl Into<String>,
11+
typed: Option<E>,
12+
parse_error: Option<&str>,
13+
) -> ApiError<E> {
14+
ApiError {
15+
status: 422,
16+
headers: HeaderMap::new(),
17+
body: body.into(),
18+
typed,
19+
parse_error: parse_error.map(str::to_owned),
20+
}
21+
}
22+
23+
#[test]
24+
fn test_api_error_display_normal_body() {
25+
let error = api_error::<TypedApiError>("small response", None, None);
26+
27+
assert_eq!(error.to_string(), "API error 422: small response");
28+
}
29+
30+
#[test]
31+
fn test_api_error_display_truncates_body_without_mutating_it() {
32+
let body = "é".repeat(600);
33+
let error = api_error::<TypedApiError>(body.clone(), None, None);
34+
let displayed = error.to_string();
35+
36+
assert_eq!(
37+
displayed,
38+
format!("API error 422: {}... [truncated]", "é".repeat(500))
39+
);
40+
assert_eq!(error.body, body);
41+
}
42+
43+
#[test]
44+
fn test_api_error_display_includes_typed_error() {
45+
let error = api_error("validation failed", Some(TypedApiError::Invalid), None);
46+
47+
assert_eq!(
48+
error.to_string(),
49+
"API error 422: validation failed; typed: Invalid"
50+
);
51+
}
52+
53+
#[test]
54+
fn test_api_error_display_includes_parse_error() {
55+
let error =
56+
api_error::<TypedApiError>("not json", None, Some("expected value at line 1 column 1"));
57+
58+
assert_eq!(
59+
error.to_string(),
60+
"API error 422: not json; parse error: expected value at line 1 column 1"
61+
);
62+
}
263

364
#[test]
465
fn test_http_error_creation() {
@@ -312,6 +373,22 @@ fn test_generated_error_code() {
312373
client_content.contains("pub fn is_retryable"),
313374
"Generated code should contain is_retryable method"
314375
);
376+
assert!(
377+
client_content.contains("API_ERROR_BODY_DISPLAY_LIMIT"),
378+
"Generated code should bound the displayed API error body"
379+
);
380+
assert!(
381+
client_content.contains("API_ERROR_BODY_TRUNCATION_MARKER"),
382+
"Generated code should include a clear body truncation marker"
383+
);
384+
assert!(
385+
client_content.contains("typed: {typed:?}"),
386+
"Generated code should display typed API error details"
387+
);
388+
assert!(
389+
client_content.contains("parse error: {parse_error}"),
390+
"Generated code should display typed parsing failures"
391+
);
315392

316393
// Verify HttpResult type alias
317394
assert!(

0 commit comments

Comments
 (0)