fix: server function custom errors set status codes (#4782)
* fix: handling status codes from errors * add demo * clippy + check * fix clippy again
This commit is contained in:
1 parent
fc069f0faf
commit
8f6e7dc1f2
9 files changed
+356
-44
No files matched your search
@@ -699,6 +699,11 @@ name = "flat_router"
|
|||||||
path = "examples/06-routing/flat_router.rs"
|
path = "examples/06-routing/flat_router.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "server_functions"
|
||||||
|
path = "examples/07-fullstack/server_functions.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "middleware"
|
name = "middleware"
|
||||||
path = "examples/07-fullstack/middleware.rs"
|
path = "examples/07-fullstack/middleware.rs"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Try running requests against the endpoints directly with `curl` or `postman` to see the actual HTTP responses!
|
//! Try running requests against the endpoints directly with `curl` or `postman` to see the actual HTTP responses!
|
||||||
|
|
||||||
use dioxus::fullstack::{Json, StatusCode};
|
use dioxus::fullstack::{AsStatusCode, Json, StatusCode};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -164,10 +164,32 @@ enum MyCustomError {
|
|||||||
ServerFnError(#[from] ServerFnError),
|
ServerFnError(#[from] ServerFnError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsStatusCode for MyCustomError {
|
||||||
|
fn as_status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
MyCustomError::BadRequest { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
MyCustomError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
MyCustomError::ServerFnError(e) => e.as_status_code(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a custom typed error (serializable) so clients can handle specific cases.
|
/// Returns a custom typed error (serializable) so clients can handle specific cases.
|
||||||
|
///
|
||||||
|
/// Our custom error must implement `AsStatusCode` so it can properly set the outgoing HTTP status code.
|
||||||
#[get("/api/typed-error")]
|
#[get("/api/typed-error")]
|
||||||
async fn get_throws_typed_error() -> Result<(), MyCustomError> {
|
async fn get_throws_typed_error() -> Result<(), MyCustomError> {
|
||||||
Err(MyCustomError::BadRequest {
|
Err(MyCustomError::BadRequest {
|
||||||
custom_name: "Invalid input".into(),
|
custom_name: "Invalid input".into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simple POST endpoint used to show a successful server function that returns `StatusCode`.
|
||||||
|
#[post("/api/data")]
|
||||||
|
async fn get_throws_serverfn_error() -> Result<(), ServerFnError> {
|
||||||
|
Err(ServerFnError::ServerError {
|
||||||
|
message: "Unauthorized access".to_string(),
|
||||||
|
code: StatusCode::UNAUTHORIZED.as_u16(),
|
||||||
|
details: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
//! This example is a simple showcase of Dioxus Server Functions.
|
||||||
|
//!
|
||||||
|
//! The other examples in this folder showcase advanced features of server functions like custom
|
||||||
|
//! data types, error handling, websockets, and more.
|
||||||
|
//!
|
||||||
|
//! This example is meant to just be a simple starting point to show how server functions work.
|
||||||
|
//!
|
||||||
|
//! ## Server Functions
|
||||||
|
//!
|
||||||
|
//! In Dioxus, Server Functions are `axum` backend endpoints that can be called directly from the client
|
||||||
|
//! as if you were simply calling a local Rust function. You can do anything with a server function
|
||||||
|
//! that an Axum handler can do like extracting path, query, headers, and body parameters.
|
||||||
|
//!
|
||||||
|
//! ## Server Function Arguments
|
||||||
|
//!
|
||||||
|
//! Unlike Axum handlers, the arguments of the server functions have some special magic enabled by
|
||||||
|
//! the accompanying `#[get]`/`#[post]` attributes. This magic enables you to choose between
|
||||||
|
//! arguments that are purely serializable (i.e. `String`, `i32`, `Vec<T>`, etc) as the JSON body of
|
||||||
|
//!
|
||||||
|
//! the request *or* arguments that implement Axum's `FromRequest` trait. This magic enables simple
|
||||||
|
//! RPC functions but also complex extractors for things like auth, sessions, cookies, and more.
|
||||||
|
//!
|
||||||
|
//! ## Server Function Return Types
|
||||||
|
//!
|
||||||
|
//! The return type of the server function is also somewhat magical. Unlike Axum handlers, all server
|
||||||
|
//! functions must return a `Result` type, giving the client an opportunity to handle errors properly.
|
||||||
|
//!
|
||||||
|
//! The `Ok` type can be anything that implements `Serialize + DeserializeOwned` so it can be sent
|
||||||
|
//! to the client as JSON, or it can be anything that implements `IntoResponse` just like an Axum handler.
|
||||||
|
//!
|
||||||
|
//! ## Error Types
|
||||||
|
//!
|
||||||
|
//! The `Err` type of the server function return type is also somewhat special. The `Err` type can be:
|
||||||
|
//! - `anyhow::Error` (the `dioxus_core::Err` type alias) for untyped errors with rich context. Note
|
||||||
|
//! that these errors will always downcast to `ServerFnError` on the client, losing the original
|
||||||
|
//! error stack and type.
|
||||||
|
//! - `ServerFnError` for typed errors with a status code and optional message.
|
||||||
|
//! - `StatusCode` for returning raw HTTP status codes.
|
||||||
|
//! - `HttpError` for returning HTTP status codes with custom messages.
|
||||||
|
//! - Any custom errors that implement `From<ServerFnError>` and are `Serialize`/`Deserialize`
|
||||||
|
//!
|
||||||
|
//! The only way to set the HTTP status code of the response is to use one of the above error types,
|
||||||
|
//! or to implement a custom `IntoResponse` type that sets the status code manually.
|
||||||
|
//!
|
||||||
|
//! The `anyhow::Error` type is the best choice for rapid development, but is somewhat limited when
|
||||||
|
//! handling specific error cases on the client since all errors are downcast to `ServerFnError`.
|
||||||
|
//!
|
||||||
|
//! ## Calling Server Functions from the Client
|
||||||
|
//!
|
||||||
|
//! Server functions can be called from the client by simply importing the function and calling it
|
||||||
|
//! like a normal Rust function. Unlike regular axum handlers, Dioxus server functions have a few
|
||||||
|
//! non-obvious restrictions.
|
||||||
|
//!
|
||||||
|
//! Most importantly, the arguments to the server function must implement either `Deserialize` *or*
|
||||||
|
//! `IntoRequest`. The `IntoRequest` trait is a Dioxus abstraction that represents the "inverse" of the
|
||||||
|
//! Axum `FromRequest` trait. Anything that is sent to the server from the client must be both extractable
|
||||||
|
//! with `FromRequest` on the server *and* constructible with `IntoRequest` on the client.
|
||||||
|
//!
|
||||||
|
//! Types like `WebsocketOptions` implement `IntoRequest` and pass along things like upgrade headers
|
||||||
|
//! to the server so that the server can properly upgrade the connection.
|
||||||
|
//!
|
||||||
|
//! When receiving data from the server, the return type must implement `Deserialize` *or* `FromResponse`.
|
||||||
|
//! The `FromResponse` trait is the inverse of Axum's `IntoResponse` trait, and is implemented
|
||||||
|
//! for types like `Websocket` where the raw HTTP response is needed to complete the construction
|
||||||
|
//! of the type.
|
||||||
|
//!
|
||||||
|
//! ## Server-only Extractors
|
||||||
|
//!
|
||||||
|
//! Because the arguments of the server function define the structure of the public API, some extractors
|
||||||
|
//! might not make sense to expose directly, nor would they be possible to construct on the client.
|
||||||
|
//! For example, on the web, you typically don't work directly with cookies since the browser handles
|
||||||
|
//! them for you. In these cases, the client would omit the `Cookie` header entirely, and we would need
|
||||||
|
//! "hoist" our extractor into a "server-only extractor".
|
||||||
|
//!
|
||||||
|
//! Server-only extractors are function arguments placed after the path in the `#[get]`/`#[post]` attribute.
|
||||||
|
//! These arguments are extracted on the server, but not passed in from the client. This lets the
|
||||||
|
//! server function remain callable from the client, while still allowing full access to axum's
|
||||||
|
//! extractors.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! #[post("/api/authenticate", auth: AuthCookie)]
|
||||||
|
//! async fn authenticate() -> Result<User> { /* ... */ }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Automatic Registration
|
||||||
|
//!
|
||||||
|
//! Unlike axum handlers, server functions do not need to be manually registered with a router.
|
||||||
|
//! By default, *all* server functions in your app will be automatically registered with the
|
||||||
|
//! server when you call `dioxus::launch` or create a router manually with `dioxus::server::router()`.
|
||||||
|
//!
|
||||||
|
//! However, not all server functions are automatically registered by default. Server functions that
|
||||||
|
//! take a `State<T>` extractor cannot be automatically added to the router since the dioxus router
|
||||||
|
//! type does not know how to construct the `T` type.
|
||||||
|
//!
|
||||||
|
//! These server functions will be registered once the `ServerFnState<T>` layer is added to the app with
|
||||||
|
//! `router = router.layer(ServerFnState::new(your_state))`.
|
||||||
|
//!
|
||||||
|
//! ## Middleware
|
||||||
|
//!
|
||||||
|
//! Middleware can be added to server functions using the `#[middleware(MiddlewareType)]` attribute.
|
||||||
|
//! Middleware will be applied in the order they are specified, and will be applied before any
|
||||||
|
//! server-only extractors.
|
||||||
|
//!
|
||||||
|
//! To add router-level middleware, you can customize the axum `Router` using layers and extensions
|
||||||
|
//! as you would in a normal axum app.
|
||||||
|
//!
|
||||||
|
//! ## Anonymous Server Functions
|
||||||
|
//!
|
||||||
|
//! The `#[server]` attribute can be used without a path to create an anonymous server function.
|
||||||
|
//! These functions are still exposed as HTTP endpoints, but their names are procedurally generated
|
||||||
|
//! from the module path, function name, and a hash of the function signature. This makes it hard to
|
||||||
|
//! call these functions with `curl` or `postman`, but save you the trouble of coming up with unique
|
||||||
|
//! names for simple functions that are only called from your Dioxus app.
|
||||||
|
//!
|
||||||
|
//! If you're shipping desktop/mobile apps, we don't recommend using anonymous server functions
|
||||||
|
//! since the function names could change between builds and thus make older versions of your app
|
||||||
|
//! incompatible with newer versions of your server.
|
||||||
|
//!
|
||||||
|
//! ## Cross-platform Clients
|
||||||
|
//!
|
||||||
|
//! Server functions can be called from any platform (web, desktop mobile, etc) and use the best
|
||||||
|
//! underlying `fetch` implementation available.
|
||||||
|
//!
|
||||||
|
//! ## More examples
|
||||||
|
//!
|
||||||
|
//! With Dioxus Fullstack 0.7, pretty much anything you can do with an Axum handler, you can do with
|
||||||
|
//! a server function. More advanced examples can be found in this folder showcasing custom data types,
|
||||||
|
//! error handling, websockets, and more.
|
||||||
|
|
||||||
|
use axum_core::response::IntoResponse;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_fullstack::FromResponse;
|
||||||
|
use dioxus_fullstack::http::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
let mut echo_action = use_action(echo);
|
||||||
|
let mut chat_action = use_action(chat);
|
||||||
|
let mut dog_data = use_action(get_data);
|
||||||
|
let mut custom_data = use_action(get_custom_data);
|
||||||
|
let mut anonymous_action = use_action(anonymous);
|
||||||
|
let mut custom_anonymous_action = use_action(custom_anonymous);
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
h1 { "Server Functions Example" }
|
||||||
|
div {
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "column",
|
||||||
|
gap: "8px",
|
||||||
|
|
||||||
|
button { onclick: move |_| echo_action.call("Hello from client".into()), "Echo: Hello" }
|
||||||
|
button { onclick: move |_| chat_action.call(42u32, Some(7u32)), "Chat (user 42, room 7)" }
|
||||||
|
button { onclick: move |_| dog_data.call(), "Get dog data" }
|
||||||
|
button { onclick: move |_| custom_data.call(), "Get custom data" }
|
||||||
|
button { onclick: move |_| anonymous_action.call(), "Call anonymous" }
|
||||||
|
button { onclick: move |_| custom_anonymous_action.call(), "Call custom anonymous" }
|
||||||
|
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
echo_action.reset();
|
||||||
|
chat_action.reset();
|
||||||
|
dog_data.reset();
|
||||||
|
custom_data.reset();
|
||||||
|
anonymous_action.reset();
|
||||||
|
custom_anonymous_action.reset();
|
||||||
|
},
|
||||||
|
"Clear results"
|
||||||
|
}
|
||||||
|
|
||||||
|
pre { "Echo result: {echo_action.result():#?}" }
|
||||||
|
pre { "Chat result: {chat_action.result():#?}" }
|
||||||
|
pre { "Dog data: {dog_data.result():#?}" }
|
||||||
|
pre { "Custom data: {custom_data.result():#?}" }
|
||||||
|
pre { "Anonymous: {anonymous_action.result():#?}" }
|
||||||
|
pre { "Custom anonymous: {custom_anonymous_action.result():#?}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain server function at a `POST` endpoint that takes a string and returns it.
|
||||||
|
/// Here, we use the `Result` return type which is an alias to `Result<T, anyhow::Error>`.
|
||||||
|
#[post("/api/echo")]
|
||||||
|
async fn echo(body: String) -> Result<String> {
|
||||||
|
Ok(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Server function that takes path and query parameters, as well as a server-only extractor.
|
||||||
|
#[post("/api/{user_id}/chat?room_id", headers: dioxus_fullstack::HeaderMap)]
|
||||||
|
async fn chat(user_id: u32, room_id: Option<u32>) -> Result<String> {
|
||||||
|
Ok(format!(
|
||||||
|
"User ID: {}, Room ID: {} - Headers: {:#?}",
|
||||||
|
user_id,
|
||||||
|
room_id.map_or("None".to_string(), |id| id.to_string()),
|
||||||
|
headers
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain server function at a `GET` endpoint that returns some JSON data. Because `DogData` is
|
||||||
|
/// `Serialize` and `Deserialize`, it can be sent to the client as JSON automatically.
|
||||||
|
///
|
||||||
|
/// You can `curl` this endpoint and it will return a 200 status code with a JSON body:
|
||||||
|
///
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "name": "Fido",
|
||||||
|
/// "age": 4
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[get("/api/dog")]
|
||||||
|
async fn get_data() -> Result<DogData> {
|
||||||
|
Ok(DogData {
|
||||||
|
name: "Fido".to_string(),
|
||||||
|
age: 4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct DogData {
|
||||||
|
name: String,
|
||||||
|
age: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A server function that returns a custom struct as JSON.
|
||||||
|
#[get("/api/custom")]
|
||||||
|
async fn get_custom_data() -> Result<CustomData> {
|
||||||
|
Ok(CustomData {
|
||||||
|
message: "Hello from the server!".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CustomData {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
impl IntoResponse for CustomData {
|
||||||
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
|
axum_core::response::Response::builder()
|
||||||
|
.status(StatusCode::ACCEPTED)
|
||||||
|
.body(serde_json::to_string(&self.message).unwrap().into())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromResponse for CustomData {
|
||||||
|
async fn from_response(res: dioxus_fullstack::ClientResponse) -> Result<Self, ServerFnError> {
|
||||||
|
let message = res.json::<String>().await?;
|
||||||
|
Ok(CustomData { message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An anonymous server function - the url path is generated from the module path and function name.
|
||||||
|
///
|
||||||
|
/// This will end up as `/api/anonymous_<hash>` where `<hash>` is a hash of the function signature.
|
||||||
|
#[server]
|
||||||
|
async fn anonymous() -> Result<String> {
|
||||||
|
Ok("Hello from an anonymous server function!".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An anonymous server function with a custom prefix and a fixed endpoint name.
|
||||||
|
///
|
||||||
|
/// This is less preferred over the `#[get]`/`#[post]` syntax but is still functional for backwards
|
||||||
|
/// compatibility. Previously, only the `#[server]` attribute was available, but as of Dioxus 0.7,
|
||||||
|
/// the `#[get]`/`#[post]` attributes are preferred for new code.
|
||||||
|
#[server(prefix = "/api/custom", endpoint = "my_anonymous")]
|
||||||
|
async fn custom_anonymous() -> Result<String> {
|
||||||
|
Ok("Hello from a custom anonymous server function!".to_string())
|
||||||
|
}
|
||||||
@@ -32,11 +32,11 @@ pub enum ServerFnError {
|
|||||||
/// representation of the error.
|
/// representation of the error.
|
||||||
#[error("error running server function: {message} (details: {details:#?})")]
|
#[error("error running server function: {message} (details: {details:#?})")]
|
||||||
ServerError {
|
ServerError {
|
||||||
|
/// A human-readable message describing the error.
|
||||||
message: String,
|
message: String,
|
||||||
|
|
||||||
/// Optional HTTP status code associated with the error.
|
/// HTTP status code associated with the error.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
code: u16,
|
||||||
code: Option<u16>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
details: Option<serde_json::Value>,
|
details: Option<serde_json::Value>,
|
||||||
@@ -89,7 +89,7 @@ impl ServerFnError {
|
|||||||
ServerFnError::ServerError {
|
ServerFnError::ServerError {
|
||||||
message: f.to_string(),
|
message: f.to_string(),
|
||||||
details: None,
|
details: None,
|
||||||
code: None,
|
code: 500,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ impl ServerFnError {
|
|||||||
|
|
||||||
ServerFnError::ServerError {
|
ServerFnError::ServerError {
|
||||||
message,
|
message,
|
||||||
code: Some(status.as_u16()),
|
code: status.as_u16(),
|
||||||
details: None,
|
details: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ impl From<anyhow::Error> for ServerFnError {
|
|||||||
ServerFnError::ServerError {
|
ServerFnError::ServerError {
|
||||||
message: value.to_string(),
|
message: value.to_string(),
|
||||||
details: None,
|
details: None,
|
||||||
code: None,
|
code: 500,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,11 +135,9 @@ impl From<serde_json::Error> for ServerFnError {
|
|||||||
impl From<ServerFnError> for http::StatusCode {
|
impl From<ServerFnError> for http::StatusCode {
|
||||||
fn from(value: ServerFnError) -> Self {
|
fn from(value: ServerFnError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
ServerFnError::ServerError { code, .. } => match code {
|
ServerFnError::ServerError { code, .. } => {
|
||||||
Some(code) => http::StatusCode::from_u16(code)
|
http::StatusCode::from_u16(code).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
}
|
||||||
None => http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
},
|
|
||||||
ServerFnError::Request(err) => match err {
|
ServerFnError::Request(err) => match err {
|
||||||
RequestError::Status(_, code) => http::StatusCode::from_u16(code)
|
RequestError::Status(_, code) => http::StatusCode::from_u16(code)
|
||||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
@@ -174,7 +172,7 @@ impl From<HttpError> for ServerFnError {
|
|||||||
.unwrap_or("Unknown error")
|
.unwrap_or("Unknown error")
|
||||||
.to_string()
|
.to_string()
|
||||||
}),
|
}),
|
||||||
code: Some(value.status.as_u16()),
|
code: value.status.as_u16(),
|
||||||
details: None,
|
details: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,9 +186,8 @@ impl IntoResponse for ServerFnError {
|
|||||||
code,
|
code,
|
||||||
details,
|
details,
|
||||||
} => {
|
} => {
|
||||||
let status = code
|
let status =
|
||||||
.and_then(|c| StatusCode::from_u16(c).ok())
|
StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
let body = if let Some(details) = details {
|
let body = if let Some(details) = details {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"error": message,
|
"error": message,
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ pub fn current_status() -> StreamingStatus {
|
|||||||
pub fn status_code_from_error(error: &CapturedError) -> StatusCode {
|
pub fn status_code_from_error(error: &CapturedError) -> StatusCode {
|
||||||
if let Some(err) = error.downcast_ref::<ServerFnError>() {
|
if let Some(err) = error.downcast_ref::<ServerFnError>() {
|
||||||
match err {
|
match err {
|
||||||
ServerFnError::ServerError {
|
ServerFnError::ServerError { code, .. } => {
|
||||||
code: Some(code), ..
|
return StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
} => return StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
}
|
||||||
_ => return StatusCode::INTERNAL_SERVER_ERROR,
|
_ => return StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,15 +258,12 @@ impl SsrRendererPool {
|
|||||||
// we need to eventually be able to downcast that and get the status code from it
|
// we need to eventually be able to downcast that and get the status code from it
|
||||||
if let Some(ServerFnError::ServerError { message, code, .. }) = error.downcast_ref()
|
if let Some(ServerFnError::ServerError { message, code, .. }) = error.downcast_ref()
|
||||||
{
|
{
|
||||||
if let Some(code) = code {
|
status_code = Some(
|
||||||
status_code = Some(
|
(*code)
|
||||||
(*code)
|
.try_into()
|
||||||
.try_into()
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
status_code = Some(StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
out_message = Some(message.clone());
|
out_message = Some(message.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ pub use client::*;
|
|||||||
pub use axum::extract::Json;
|
pub use axum::extract::Json;
|
||||||
pub use axum::response::{NoContent, Redirect};
|
pub use axum::response::{NoContent, Redirect};
|
||||||
|
|
||||||
|
pub use crate::request::{FromResponse, FromResponseParts};
|
||||||
|
|
||||||
pub use payloads::*;
|
pub use payloads::*;
|
||||||
pub mod payloads {
|
pub mod payloads {
|
||||||
use crate::{ClientRequest, ClientResponse, ClientResult, IntoRequest};
|
use crate::{ClientRequest, ClientResponse, ClientResult, IntoRequest};
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ pub enum RestEndpointPayload<T, E> {
|
|||||||
pub struct ErrorPayload<E> {
|
pub struct ErrorPayload<E> {
|
||||||
message: String,
|
message: String,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
code: u16,
|
||||||
code: Option<u16>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
data: Option<E>,
|
data: Option<E>,
|
||||||
@@ -280,7 +279,7 @@ mod decode_ok {
|
|||||||
if let Ok(text) = String::from_utf8(as_bytes.to_vec()) {
|
if let Ok(text) = String::from_utf8(as_bytes.to_vec()) {
|
||||||
Ok(RestEndpointPayload::Error(ErrorPayload {
|
Ok(RestEndpointPayload::Error(ErrorPayload {
|
||||||
message: format!("HTTP {}: {}", status.as_u16(), text),
|
message: format!("HTTP {}: {}", status.as_u16(), text),
|
||||||
code: Some(status.as_u16()),
|
code: status.as_u16(),
|
||||||
data: None,
|
data: None,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@@ -397,8 +396,10 @@ mod decode_ok {
|
|||||||
message: _message,
|
message: _message,
|
||||||
details: _details,
|
details: _details,
|
||||||
code,
|
code,
|
||||||
} => Err(StatusCode::from_u16(code.unwrap_or(500))
|
} => {
|
||||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
|
Err(StatusCode::from_u16(code)
|
||||||
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
|
||||||
|
}
|
||||||
|
|
||||||
ServerFnError::Registration(_) | ServerFnError::MiddlewareError(_) => {
|
ServerFnError::Registration(_) | ServerFnError::MiddlewareError(_) => {
|
||||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
@@ -439,7 +440,7 @@ mod decode_ok {
|
|||||||
Ok(Ok(res)) => Ok(res),
|
Ok(Ok(res)) => Ok(res),
|
||||||
Ok(Err(res)) => match res {
|
Ok(Err(res)) => match res {
|
||||||
ServerFnError::ServerError { message, code, .. } => Err(HttpError {
|
ServerFnError::ServerError { message, code, .. } => Err(HttpError {
|
||||||
status: StatusCode::from_u16(code.unwrap_or(500))
|
status: StatusCode::from_u16(code)
|
||||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
message: Some(message),
|
message: Some(message),
|
||||||
}),
|
}),
|
||||||
@@ -617,16 +618,33 @@ mod resp {
|
|||||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response>;
|
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the status code from the error type if possible.
|
||||||
|
pub trait AsStatusCode {
|
||||||
|
fn as_status_code(&self) -> StatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsStatusCode for ServerFnError {
|
||||||
|
fn as_status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Self::ServerError { code, .. } => {
|
||||||
|
StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, E> MakeAxumError<E> for &&&ServerFnDecoder<Result<T, E>>
|
impl<T, E> MakeAxumError<E> for &&&ServerFnDecoder<Result<T, E>>
|
||||||
where
|
where
|
||||||
E: From<ServerFnError> + Serialize + DeserializeOwned + Display,
|
E: AsStatusCode + From<ServerFnError> + Serialize + DeserializeOwned + Display,
|
||||||
{
|
{
|
||||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response> {
|
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response> {
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let status_code = err.as_status_code();
|
||||||
let err = ErrorPayload {
|
let err = ErrorPayload {
|
||||||
code: None,
|
code: status_code.as_u16(),
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
data: Some(err),
|
data: Some(err),
|
||||||
};
|
};
|
||||||
@@ -636,7 +654,7 @@ mod resp {
|
|||||||
http::header::CONTENT_TYPE,
|
http::header::CONTENT_TYPE,
|
||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
*resp.status_mut() = status_code;
|
||||||
Err(resp)
|
Err(resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +683,7 @@ mod resp {
|
|||||||
},
|
},
|
||||||
Ok(other) => ErrorPayload {
|
Ok(other) => ErrorPayload {
|
||||||
message: other.to_string(),
|
message: other.to_string(),
|
||||||
code: None,
|
code: 500,
|
||||||
data: None,
|
data: None,
|
||||||
},
|
},
|
||||||
Err(err) => match err.downcast::<HttpError>() {
|
Err(err) => match err.downcast::<HttpError>() {
|
||||||
@@ -673,11 +691,11 @@ mod resp {
|
|||||||
message: http_err
|
message: http_err
|
||||||
.message
|
.message
|
||||||
.unwrap_or_else(|| http_err.status.to_string()),
|
.unwrap_or_else(|| http_err.status.to_string()),
|
||||||
code: Some(http_err.status.as_u16()),
|
code: http_err.status.as_u16(),
|
||||||
data: None,
|
data: None,
|
||||||
},
|
},
|
||||||
Err(err) => ErrorPayload {
|
Err(err) => ErrorPayload {
|
||||||
code: None,
|
code: 500,
|
||||||
message: err.to_string(),
|
message: err.to_string(),
|
||||||
data: None,
|
data: None,
|
||||||
},
|
},
|
||||||
@@ -706,7 +724,7 @@ mod resp {
|
|||||||
Ok(resp) => Ok(resp),
|
Ok(resp) => Ok(resp),
|
||||||
Err(status) => {
|
Err(status) => {
|
||||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||||
code: Some(status.as_u16()),
|
code: status.as_u16(),
|
||||||
message: status.to_string(),
|
message: status.to_string(),
|
||||||
data: None,
|
data: None,
|
||||||
})
|
})
|
||||||
@@ -732,7 +750,7 @@ mod resp {
|
|||||||
Ok(resp) => Ok(resp),
|
Ok(resp) => Ok(resp),
|
||||||
Err(http_err) => {
|
Err(http_err) => {
|
||||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||||
code: Some(http_err.status.as_u16()),
|
code: http_err.status.as_u16(),
|
||||||
message: http_err
|
message: http_err
|
||||||
.message
|
.message
|
||||||
.unwrap_or_else(|| http_err.status.to_string()),
|
.unwrap_or_else(|| http_err.status.to_string()),
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ impl NavigationProvider for DioxusNativeNavigationProvider {
|
|||||||
if options.method == Method::GET
|
if options.method == Method::GET
|
||||||
&& matches!(options.url.scheme(), "http" | "https" | "mailto")
|
&& matches!(options.url.scheme(), "http" | "https" | "mailto")
|
||||||
{
|
{
|
||||||
if let Err(err) = webbrowser::open(options.url.as_str()) {
|
if let Err(_err) = webbrowser::open(options.url.as_str()) {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::error!("Failed to open URL: {}", err);
|
tracing::error!("Failed to open URL: {}", _err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user