Skip to content

Commit ceb8261

Browse files
authored
fix: Handle HTTP errors in FromResponseParts (#5243)
* fix: Handle HTTP errors in FromResponseParts Check for non-success status codes before attempting to decode response parts. For error responses, parse the JSON error payload and convert it to a ServerFnError::ServerError. Also make ErrorPayload fields pub(crate) to allow internal access. * fixed test name
1 parent 7a7f5fb commit ceb8261

2 files changed

Lines changed: 99 additions & 7 deletions

File tree

packages/fullstack/src/magic.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ pub enum RestEndpointPayload<T, E> {
8787
/// The error payload structure for REST API errors.
8888
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
8989
pub struct ErrorPayload<E> {
90-
message: String,
90+
pub(crate) message: String,
9191

92-
code: u16,
92+
pub(crate) code: u16,
9393

9494
#[serde(skip_serializing_if = "Option::is_none")]
95-
data: Option<E>,
95+
pub(crate) data: Option<E>,
9696
}
9797

9898
/// Convert a `RequestError` into a `ServerFnError`.

packages/fullstack/src/request.rs

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use headers::Header;
44
use http::response::Parts;
55
use std::{future::Future, pin::Pin};
66

7-
use crate::{ClientRequest, ClientResponse};
7+
use crate::{ClientRequest, ClientResponse, ErrorPayload};
88

99
/// The `IntoRequest` trait allows types to be used as the body of a request to a HTTP endpoint or server function.
1010
///
@@ -54,9 +54,24 @@ where
5454
{
5555
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
5656
async move {
57-
let (parts, _body) = res.into_parts();
58-
let mut parts = parts;
59-
A::from_response_parts(&mut parts)
57+
let status = res.status();
58+
59+
if status.is_success() {
60+
let (parts, _body) = res.into_parts();
61+
let mut parts = parts;
62+
A::from_response_parts(&mut parts)
63+
} else {
64+
let ErrorPayload::<serde_json::Value> {
65+
message,
66+
code,
67+
data,
68+
} = res.json().await?;
69+
Err(ServerFnError::ServerError {
70+
message,
71+
code,
72+
details: data,
73+
})
74+
}
6075
}
6176
}
6277
}
@@ -202,3 +217,80 @@ pub fn assert_can_encode(_t: impl AssertCanEncode) {}
202217

203218
#[doc(hidden)]
204219
pub fn assert_can_decode(_t: impl AssertCanDecode) {}
220+
221+
#[cfg(test)]
222+
mod test {
223+
use http::Extensions;
224+
225+
use super::*;
226+
227+
#[derive(Debug)]
228+
struct TestFromResponse;
229+
230+
impl FromResponseParts for TestFromResponse {
231+
fn from_response_parts(_parts: &mut Parts) -> Result<Self, ServerFnError> {
232+
Ok(Self)
233+
}
234+
}
235+
236+
fn build_response(status: u16, body: String) -> ClientResponse {
237+
let http_response = http::Response::builder()
238+
.status(status)
239+
.body(body.into_bytes())
240+
.unwrap();
241+
let reqwest_response = reqwest::Response::from(http_response);
242+
ClientResponse {
243+
response: Box::new(reqwest_response),
244+
extensions: Extensions::new(),
245+
}
246+
}
247+
248+
#[test]
249+
fn fromresponseparts_path_decodes_ok_on_2xx() {
250+
futures::executor::block_on(async {
251+
let response = build_response(200, "".to_string());
252+
let result = TestFromResponse::from_response(response).await;
253+
assert!(
254+
result.is_ok(),
255+
"expected Ok(..) for HTTP 200 success case, got: {:?}",
256+
result
257+
);
258+
});
259+
}
260+
261+
#[test]
262+
fn fromresponseparts_falls_back_to_request_error_on_unparsable_error_body() {
263+
futures::executor::block_on(async {
264+
let response = build_response(400, "".to_string());
265+
let result = TestFromResponse::from_response(response).await;
266+
assert!(result.is_err(), "expected Err(..) for HTTP 400 failed case");
267+
let error = result.unwrap_err();
268+
assert!(matches!(
269+
error,
270+
ServerFnError::Request(RequestError::Decode(_))
271+
));
272+
});
273+
}
274+
275+
#[test]
276+
fn fromresponseparts_parses_error_payload_on_http_error() {
277+
futures::executor::block_on(async {
278+
let body = r#"{
279+
"message": "qwerty",
280+
"code": 400
281+
}"#;
282+
let response = build_response(400, body.to_string());
283+
let result = TestFromResponse::from_response(response).await;
284+
assert!(result.is_err(), "expected Err(..) for HTTP 400 failed case");
285+
let error = result.unwrap_err();
286+
assert_eq!(
287+
error,
288+
ServerFnError::ServerError {
289+
message: "qwerty".to_string(),
290+
code: 400,
291+
details: None
292+
}
293+
);
294+
});
295+
}
296+
}

0 commit comments

Comments
 (0)