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:
Jonathan Kelley authored and GitHub committed 2025-10-13 16:02:33 -07:00
1 parent fc069f0faf
commit 8f6e7dc1f2
9 files changed
+356 -44

No files matched your search

+5
View File
@@ -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"
+23 -1
View File
@@ -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,
})
}
+271
View File
@@ -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())
}
+12 -15
View File
@@ -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,
+3 -3
View File
@@ -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,
} }
} }
+6 -9
View File
@@ -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());
} }
+2
View File
@@ -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};
+32 -14
View File
@@ -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()),
+2 -2
View File
@@ -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);
} }
} }
} }