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
This commit is contained in:
1 parent
7a7f5fb719
commit
ceb82612e0
2 files changed
+99
-7
No files matched your search
@@ -87,12 +87,12 @@ pub enum RestEndpointPayload<T, E> {
|
|||||||
/// The error payload structure for REST API errors.
|
/// The error payload structure for REST API errors.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ErrorPayload<E> {
|
pub struct ErrorPayload<E> {
|
||||||
message: String,
|
pub(crate) message: String,
|
||||||
|
|
||||||
code: u16,
|
pub(crate) code: u16,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
data: Option<E>,
|
pub(crate) data: Option<E>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a `RequestError` into a `ServerFnError`.
|
/// Convert a `RequestError` into a `ServerFnError`.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use headers::Header;
|
|||||||
use http::response::Parts;
|
use http::response::Parts;
|
||||||
use std::{future::Future, pin::Pin};
|
use std::{future::Future, pin::Pin};
|
||||||
|
|
||||||
use crate::{ClientRequest, ClientResponse};
|
use crate::{ClientRequest, ClientResponse, ErrorPayload};
|
||||||
|
|
||||||
/// The `IntoRequest` trait allows types to be used as the body of a request to a HTTP endpoint or server function.
|
/// The `IntoRequest` trait allows types to be used as the body of a request to a HTTP endpoint or server function.
|
||||||
///
|
///
|
||||||
@@ -54,9 +54,24 @@ where
|
|||||||
{
|
{
|
||||||
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
|
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
|
||||||
async move {
|
async move {
|
||||||
let (parts, _body) = res.into_parts();
|
let status = res.status();
|
||||||
let mut parts = parts;
|
|
||||||
A::from_response_parts(&mut parts)
|
if status.is_success() {
|
||||||
|
let (parts, _body) = res.into_parts();
|
||||||
|
let mut parts = parts;
|
||||||
|
A::from_response_parts(&mut parts)
|
||||||
|
} else {
|
||||||
|
let ErrorPayload::<serde_json::Value> {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
data,
|
||||||
|
} = res.json().await?;
|
||||||
|
Err(ServerFnError::ServerError {
|
||||||
|
message,
|
||||||
|
code,
|
||||||
|
details: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,3 +217,80 @@ pub fn assert_can_encode(_t: impl AssertCanEncode) {}
|
|||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn assert_can_decode(_t: impl AssertCanDecode) {}
|
pub fn assert_can_decode(_t: impl AssertCanDecode) {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use http::Extensions;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestFromResponse;
|
||||||
|
|
||||||
|
impl FromResponseParts for TestFromResponse {
|
||||||
|
fn from_response_parts(_parts: &mut Parts) -> Result<Self, ServerFnError> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_response(status: u16, body: String) -> ClientResponse {
|
||||||
|
let http_response = http::Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.body(body.into_bytes())
|
||||||
|
.unwrap();
|
||||||
|
let reqwest_response = reqwest::Response::from(http_response);
|
||||||
|
ClientResponse {
|
||||||
|
response: Box::new(reqwest_response),
|
||||||
|
extensions: Extensions::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fromresponseparts_path_decodes_ok_on_2xx() {
|
||||||
|
futures::executor::block_on(async {
|
||||||
|
let response = build_response(200, "".to_string());
|
||||||
|
let result = TestFromResponse::from_response(response).await;
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"expected Ok(..) for HTTP 200 success case, got: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fromresponseparts_falls_back_to_request_error_on_unparsable_error_body() {
|
||||||
|
futures::executor::block_on(async {
|
||||||
|
let response = build_response(400, "".to_string());
|
||||||
|
let result = TestFromResponse::from_response(response).await;
|
||||||
|
assert!(result.is_err(), "expected Err(..) for HTTP 400 failed case");
|
||||||
|
let error = result.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
ServerFnError::Request(RequestError::Decode(_))
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fromresponseparts_parses_error_payload_on_http_error() {
|
||||||
|
futures::executor::block_on(async {
|
||||||
|
let body = r#"{
|
||||||
|
"message": "qwerty",
|
||||||
|
"code": 400
|
||||||
|
}"#;
|
||||||
|
let response = build_response(400, body.to_string());
|
||||||
|
let result = TestFromResponse::from_response(response).await;
|
||||||
|
assert!(result.is_err(), "expected Err(..) for HTTP 400 failed case");
|
||||||
|
let error = result.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
ServerFnError::ServerError {
|
||||||
|
message: "qwerty".to_string(),
|
||||||
|
code: 400,
|
||||||
|
details: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user