JonasKruckenberg / tauri-sys

Bindings to the Tauri API for projects using wasm-bindgen
Apache License 2.0
84 stars 21 forks source link

Enabling to handle a received error from tauri #18

Open usagrada opened 1 year ago

usagrada commented 1 year ago

Error handling is needed with commands like invoke, so I want to be able to handle the type on error as well (Users don't know when the error occurred). Therefore, I want to add enum ResponseError (I don't think this implementation is the best, so I will be fine when the same thing is implemented).

Now, type of error is private, so it is a little helpful only that tauri-sys::Error become public (Maybe it is work in progress). I am happy that you will consider it.

// simple example(return error from tauri)
#[tauri::command]
async fn hello() -> Result<(), String> {
    Err(String::from("error occur!!"))
}
// frontend
let res = invoke::<_, String, String>("hello", &()).await;
match res {
  Ok(data) => {
    log::info!("Success tauri command: {}", data);
  }
  Err(e) => {
    use tauri_sys::tauri::ResponseError;
    match e {
      ResponseError::ReceivedError { error } => {
        log::error!("Error tauri command: {}", error);
      }
      _ => {}
    }
  }
JonasKruckenberg commented 1 year ago

Yeah so the Error type is neither public nor very stable right now. But I get the use case. Implementing an ReceivedError such as you proposed seems a bit redundant since tauri_sys::Error will cover all these cases and be more flexible. I'll try to prioritise fixing up the error enum. Maybe you are also willing to improve the error messages, feel free to open a PR in that case!

usagrada commented 1 year ago

Thank you for comment. Right, this implementation is redundant and I also think that covering all these cases in tauri_sys::Error is better way. Fixing up the error enum is very helpful for me! The error type in simple example is String, but when that is custom type like struct, I feared that changing tauri_sys::Error would have a widespread effect because many files uses this type.

When some good implementation, I will comment in this thread and will open PR.

JonasKruckenberg commented 1 year ago

Wait a second, maybe I was misunderstanding. Your feature request is basically being able to have structured errors on the Core side that can be interpreted as structured errors on the client too?

So like this: Core:

#[derive(Serialize)]
struct Error {
 code: u32,
 message: String
}

#[tauri::command]
fn cmd() -> Result<(), Error> {
 todo!();
}

Client:

#[derive(Deserialize)]
struct Error {
 code: u32,
 message: String
}

let res = invoke::<(), Error>("cmd").await?;
usagrada commented 1 year ago

Yes! Sorry that my example is bad for understanding. Your example represents my thoughts correctly. I want to handle not only String type but also custom type error in client side.

JonasKruckenberg commented 1 year ago

Okay yeah but that actually does not require any changes to tauri-sys afaik. What you can do is the following:

  1. create another crate in the same workspace, call it shared or types or whatever
  2. Define the error enum you want, it must derive serde::Serialize and serde::Deserialize but ideally you also use a crate for nicer error handling such as thiserror
  3. Now you can import the error enum from the shared crate both in the Core and the Client
  4. Now you call invoke like this:
    
    let res = invoke::<(), Result<T, Error>>("command").await;

match res { Ok(res) => match res { Ok(t) => println!("command returned successful {}", t), Err(e) => println!("command returned error {:?}", e) } Err(e) => panic!("Some issue with deserialising or Tauri") }


or a bit shorter using `?`

```rust
let res = invoke::<(), Result<T, Error>>("command").await?; // use ? here to bubble up the tauri-sys errors

Ok(res) => match res {
   Ok(t) => println!("command returned successful {}", t),
   Err(e) => println!("command returned error {:?}", e)
}
usagrada commented 1 year ago

I appreciate your advice, but your code did not work in my environments(My setting is wrong?). When command returned error, res is Err(Error::Binding(~)) not nested Result type value. Could you check this problem?

let res = invoke::<(), Result<T, Error>>("command").await;

match res {
   Ok(res) => match res {
      Ok(t) => println!("command returned successful {}", t),
      Err(e) => println!("command returned error {:?}", e)
   }
   Err(e) => panic!("Some issue with deserialising or Tauri") // matching now
}
JonasKruckenberg commented 1 year ago

Okay yeah so this is down to how the JSON serialisation and invoke kinda "flattens" results, not much we can do about this immediately :/ but making tauri_sys::Error public should allow you to differentiate at least. Maybe the binding variant can keep the JsValue for you to deserialise manually or something

usagrada commented 1 year ago

I see…… I agree that making tauri_sys::Error public is a quick fix for this problem.

As extending tauri_sys::Error, I re-implemented. It also requires making tauri_sys::Error public. How is it? url

farmisen commented 1 year ago

Okay yeah so this is down to how the JSON serialisation and invoke kinda "flattens" results, not much we can do about this immediately :/ but making tauri_sys::Error public should allow you to differentiate at least. Maybe the binding variant can keep the JsValue for you to deserialise manually or something

The following worked perfectly for me after making the error module public:

Shared crate:

#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum Error {
    #[error("unauthorized user")]
    UnauthorizedUserError,

    #[error("unknown error")]
    UnknownError,

    #[error("wrapped tauri-sys::Error error")]
    TauriSysError(String),
    #[error("not a tauri-sys::Error::Binding error")]
    NotABindingError,
    #[error("tauri-sys::Error::Binding deserialization error")]
    BindingDeserializationError,
}

impl Error {
    pub fn from_binding(value: String) -> Self {
        let re = Regex::new(r#"JsValue\((?P<error>"\w+")\)"#).unwrap();
        match re.captures(&value) {
            Some(caps) => match caps["error"].parse::<String>() {
                Ok(str) => match serde_json::from_str::<Error>(str.as_str()) {
                    Ok(error) => error,
                    _ => Error::BindingDeserializationError,
                },
                _ => unreachable!(),
            },
            _ => Error::NotABindingError,
        }
    }
}

Backend:

#[tauri::command]
pub async fn sign_in(email: String, password: String) -> Result<UserInfoDTO, Error> {
    ...

    match response_info.status {
        200 => {
            Ok(UserInfoDTO { signed_in: true })
        }
        _ => Err(Error::UnauthorizedUserError),
    }
}

Frontend:

pub async fn sign_in_command(email: String, password: String) -> Result<UserInfoDTO, Error> {
    let res =
        tauri::invoke::<SignInCmdArgs, UserInfoDTO>("sign_in", &SignInCmdArgs { email, password })
            .await;
    handle_backend_result::<UserInfoDTO>(res)
}

fn handle_backend_result<T>(result: Result<T, tauri_sys::error::Error>) -> Result<T, Error> {
    match result {
        Ok(result) => Ok(result),
        Err(tauri_sys::error::Error::Binding(string)) => Err(Error::from_binding(string)),
        Err(e) => Err(Error::TauriSysError(e.to_string())),
    }
}
usagrada commented 1 year ago

Thank you for advice. Your code maybe works truly, but I don't think users handling library error is good idea. I think adding third parameter in generics for error type is better idea (These codes become panic when users pass wrong type, but work truly when users pass same type in frontend and backend.).

type Response<T, E> = Result<T, E>;

#[inline(always)]
pub async fn invoke<A: Serialize, R: DeserializeOwned, E: DeserializeOwned>(
    cmd: &str,
    args: &A,
) -> Response<R, E> {
    let raw = inner::invoke(
        cmd,
        serde_wasm_bindgen::to_value(args).expect("serde binding error: args"),
    )
    .await;
    match raw {
        Ok(raw) => {
            let res: R = serde_wasm_bindgen::from_value(raw).expect("serde binding error");
            Ok(res)
        }
        Err(e) => {
            let error: E = serde_wasm_bindgen::from_value(e).expect("serde binding error");
            Err(error)
        }
    }
}
// error.rs
use serde::de::DeserializeOwned;
use wasm_bindgen::JsValue;

#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)]
pub enum Error<T: DeserializeOwned = String> {
    #[error("TODO.")]
    Binding(String),
    #[error("TODO.")]
    Serde(String),
    #[cfg(any(feature = "event", feature = "window"))]
    #[error("TODO.")]
    OneshotCanceled(#[from] futures::channel::oneshot::Canceled),
    #[error("custom error TODO.")]
    CustomError(T),
}

impl<T: DeserializeOwned> From<serde_wasm_bindgen::Error> for Error<T> {
    fn from(e: serde_wasm_bindgen::Error) -> Self {
        Self::Serde(e.to_string())
    }
}

impl<T: DeserializeOwned> From<JsValue> for Error<T> {
    fn from(e: JsValue) -> Self {
        Self::Binding(format!("{:?}", e))
    }
}

// tauri.rs
type Response<T, E> = Result<T, crate::Error<E>>;

#[inline(always)]
pub async fn invoke<A: Serialize, R: DeserializeOwned, E: DeserializeOwned>(
    cmd: &str,
    args: &A,
) -> Response<R, E> {
    let raw = inner::invoke(
        cmd,
        serde_wasm_bindgen::to_value(args).expect("serde binding error"),
    )
    .await;
    match raw {
        Ok(raw) => {
            let res = serde_wasm_bindgen::from_value(raw);
            match res {
                Ok(res) => Ok(res),
                Err(e) => Err(crate::Error::from(e)),
            }
        }
        Err(e) => {
            let error = serde_wasm_bindgen::from_value(e);
            match error {
                Ok(e) => Err(crate::Error::CustomError(e)),
                Err(e) => Err(crate::Error::from(e)),
            }
        }
    }
}
bicarlsen commented 1 year ago

Agreed that tauri_sys::Error should become public.

CorvusPrudens commented 8 months ago

Is the scope of this change large? It seems like a pretty fundamental requirement.