Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ bevy = "0.17"
# a fork of pretty please for tests - let's get off of this if we can!
prettier-please = { version = "0.3.0", features = ["verbatim"] }
anyhow = "1.0.98"
miette = { version = "7.2.0", features = ["serde"] }
clap = { version = "4.5.40" }
askama_escape = "0.13.0"
tracing = "0.1.41"
Expand Down
1 change: 1 addition & 0 deletions examples/01-app-demos/hotdog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rusqlite = { version = "0.32.0", optional = true }
anyhow = { workspace = true }
miette = { workspace = true }

[features]
default = ["web", "server"]
Expand Down
11 changes: 6 additions & 5 deletions examples/01-app-demos/hotdog/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Result;
use dioxus::prelude::*;
use miette::IntoDiagnostic;

#[cfg(feature = "server")]
thread_local! {
Expand All @@ -19,7 +19,7 @@ thread_local! {
}

#[get("/api/dogs")]
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
pub async fn list_dogs() -> anyhow::Result<Vec<(usize, String)>> {
DB.with(|db| {
Ok(db
.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
Expand All @@ -29,13 +29,14 @@ pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
}

#[delete("/api/dogs/{id}")]
pub async fn remove_dog(id: usize) -> Result<()> {
pub async fn remove_dog(id: usize) -> anyhow::Result<()> {
DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
Ok(())
}

#[post("/api/dogs")]
pub async fn save_dog(image: String) -> Result<()> {
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
pub async fn save_dog(image: String) -> miette::Result<()> {
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))
.into_diagnostic()?;
Ok(())
}
1 change: 1 addition & 0 deletions packages/fullstack-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ thiserror = { workspace = true }
axum-core = { workspace = true }
http = { workspace = true }
anyhow = { workspace = true }
miette = { workspace = true }
inventory = { workspace = true }
serde_json = { workspace = true }
generational-box = { workspace = true }
Expand Down
18 changes: 18 additions & 0 deletions packages/fullstack-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use axum_core::response::IntoResponse;
use futures_util::TryStreamExt;
use http::StatusCode;
use miette::MietteDiagnostic;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;

Expand Down Expand Up @@ -126,6 +127,23 @@ impl From<anyhow::Error> for ServerFnError {
}
}

impl From<miette::Report> for ServerFnError {
fn from(value: miette::Report) -> Self {
ServerFnError::ServerError {
message: value.to_string(),
details: Some(serde_json::json!(MietteDiagnostic {
message: value.to_string(),
code: value.code().map(|c| c.to_string()),
severity: value.severity(),
help: value.help().map(|h| h.to_string()),
url: value.url().map(|u| u.to_string()),
labels: value.labels().map(|labels| labels.collect::<Vec<_>>()),
})),
code: 500,
}
}
}

impl From<serde_json::Error> for ServerFnError {
fn from(value: serde_json::Error) -> Self {
ServerFnError::Deserialization(value.to_string())
Expand Down
10 changes: 10 additions & 0 deletions packages/fullstack-core/src/httperror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ impl<T> OrHttpError<T, AnyhowMarker> for Result<T, anyhow::Error> {
}
}

pub struct MietteMarker;
impl<T> OrHttpError<T, MietteMarker> for Result<T, miette::Report> {
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
self.map_err(|_| HttpError {
status,
message: Some(message.into()),
})
}
}

impl IntoResponse for HttpError {
fn into_response(self) -> axum_core::response::Response {
let body = match &self.message {
Expand Down
2 changes: 2 additions & 0 deletions packages/fullstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ thiserror = { workspace = true }
dioxus-fullstack-core = { workspace = true }
http = { workspace = true }
anyhow = { workspace = true }
miette = { workspace = true }
dioxus-core = { workspace = true }
dioxus-signals = { workspace = true }
dioxus-hooks = { workspace = true }
Expand Down Expand Up @@ -89,6 +90,7 @@ tokio-util = { workspace = true, features = ["codec", "compat"] }
[dev-dependencies]
dioxus = { workspace = true, features = ["fullstack", "router"] }
dioxus-server = { workspace = true }
miette = { workspace = true }

[features]
default = ["ws"]
Expand Down
70 changes: 70 additions & 0 deletions packages/fullstack/src/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,35 @@ mod decode_ok {
}
}

/// Here we convert to ServerFnError and then into the miette::Report, letting the user downcast
/// from the ServerFnError if they want to.
///
/// This loses any actual type information, but is the most flexible for users.
impl<T> RequestDecodeErr<T, miette::Report> for &ServerFnDecoder<Result<T, miette::Report>> {
fn decode_client_err(
&self,
res: Result<Result<T, ServerFnError>, RequestError>,
) -> impl Future<Output = Result<T, miette::Report>> + Send {
SendWrapper::new(async move {
match res {
Ok(Ok(res)) => Ok(res),
Ok(Err(ServerFnError::ServerError {
message,
details: Some(details),
..
})) => {
let diag = serde_json::from_value::<miette::MietteDiagnostic>(details)
.unwrap_or(miette::MietteDiagnostic::new(message.clone()));
let report = miette::Report::new(diag);
Err(report)
}
Ok(Err(e)) => Err(miette::Report::from_err(e)),
Err(err) => Err(miette::Report::from_err(err)),
}
})
}
}

/// This converts to statuscode, which can be useful but loses a lot of information.
impl<T> RequestDecodeErr<T, StatusCode> for &ServerFnDecoder<Result<T, StatusCode>> {
fn decode_client_err(
Expand Down Expand Up @@ -715,6 +744,47 @@ mod resp {
}
}

impl<T> MakeAxumError<miette::Report> for &ServerFnDecoder<Result<T, miette::Report>> {
fn make_axum_error(
self,
result: Result<Response, miette::Report>,
) -> Result<Response, Response> {
match result {
Ok(res) => Ok(res),
Err(err) => {
// Convert miette::Report to ServerFnError, then to ErrorPayload
let server_fn_error = ServerFnError::from(err);
let payload = match server_fn_error {
ServerFnError::ServerError {
message,
code,
details,
} => ErrorPayload {
message,
code,
data: details,
},
other => ErrorPayload {
message: other.to_string(),
code: 500,
data: None,
},
};

let body = serde_json::to_string(&payload).unwrap();
let mut resp = Response::new(body.into());
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
*resp.status_mut() = StatusCode::from_u16(payload.code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
Err(resp)
}
}
}
}

impl<T> MakeAxumError<StatusCode> for &&ServerFnDecoder<Result<T, StatusCode>> {
fn make_axum_error(
self,
Expand Down
6 changes: 6 additions & 0 deletions packages/fullstack/tests/compile-test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ mod simple_extractors {
Ok(Bytes::from_static(b"Hello!"))
}

/// We can use the miette error type
#[get("/hello")]
async fn nine_b() -> miette::Result<Bytes> {
Ok(Bytes::from_static(b"Hello!"))
}

/// We can use the ServerFnError error type
#[get("/hello")]
async fn ten() -> Result<Bytes, ServerFnError> {
Expand Down