Live data from Hacker News

Implementing Rust newtype for errors in axum

rup12.net

1–10 of 11 posts

Re: Implementing Rust newtype for errors in axum

#4
would you get backtraces with this implementation? I've been increasingly frustrated with axum. It hits my custom error where I print something like "sqlx database failed" but when there's lots of traffic it's hard to determine which of the hundreds of queries actually failed. What I really want is the top part of a backtrace or at least a line number. I know anyhow gives bts but will it actually be useful when used like this?

Anyone else have ideas on how to solve this?

Re: Implementing Rust newtype for errors in axum

#5
post #4

would you get backtraces with this implementation? I've been increasingly frustrated with axum. It hits my custom error where I print something like "sqlx database failed" but when there's lots of traffic it's hard to determine which of the hundreds of queries actually failed. What I really want is the top part of a backtrace or at least a line number. I know anyhow gives bts but will it actually be useful when used…

i do not see problems with axum.

we wrote some macro(with muncher) which allows strongly typed and yet dry per method error, error split of public part vs internal log only(with backtrace crate trace capture), full openapi support(via schemars) and we do not use anyhow.

whole article is rot and bad advice.

Re: Implementing Rust newtype for errors in axum

#6
post #5
post #4

would you get backtraces with this implementation? I've been increasingly frustrated with axum. It hits my custom error where I print something like "sqlx database failed" but when there's lots of traffic it's hard to determine which of the hundreds of queries actually failed. What I really want is the top part of a backtrace or at least a line number. I know anyhow gives bts but will it actually be useful when used…

i do not see problems with axum. we wrote some macro(with muncher) which allows strongly typed and yet dry per method error, error split of public part vs internal log only(with backtrace crate trace capture), full openapi support(via schemars) and we do not use anyhow. whole article is rot and bad advice.

mind sharing a link?

Re: Implementing Rust newtype for errors in axum

#8

Great post. I do the same thing for all of my Rust web servers too. I don't use anyhow though, prefer rolling everything myself - which as you call out is a pain after a while.

`Anyhow` is a great thing to get you started. But ultimately you wanna use some bespoke errors, and just roll your own!

Re: Implementing Rust newtype for errors in axum

#10
post #6
post #5

Earlier quoted context omitted.

i do not see problems with axum. we wrote some macro(with muncher) which allows strongly typed and yet dry per method error, error split of public part vs internal log only(with backtrace crate trace capture), full openapi support(via schemars) and we do not use anyhow. whole article is rot and bad advice.

mind sharing a link?

used like this

``` pub async fn account_pubkey( GetAccountPubkey { account_id }: GetAccountPubkey, State(st): State, ) -> Response!(RegistrationKey; not_found: UserNotFound) { ```

``` pub async fn action( SubmitAction {}: SubmitAction, State(st): State, TypedHeader(content_type): TypedHeader, body: axum::body::Bytes, ) -> Result { ```

as you see limitation of ast macro - single entity per code. to do more - need proc macro.

default response is json

``` #[macro_export] macro_rules! Response { ($ty:ty) => { Result, $crate::http::error::Error> }; ($ty:ty; $($var:ident : $e:ty),) => { Result, nord_core::ApiError!($($var : $e),)> }; } ```

use ApiError for other mime types

``` #[macro_export] macro_rules! ApiError { ( $($var:ident : $e:ty),* $(,)? ) => { nord_core::ApiError!({ $($var : $e,)* }) }; () => { $crate::http::error::Error:: }; ({ $($var:ident : $e:ty),* $(,)? }) => { nord_core::ApiError!( @internal bad_request: $crate::http::error::Infallible, not_found: $crate::http::error::Infallible, forbidden: $crate::http::error::Infallible, unsupported_media_type: $crate::http::error::Infallible, payload_too_large: $crate::http::error::Infallible, not_implemented: $crate::http::error::Infallible, | $($var : $e,)* ) }; ( @internal bad_request: $bad_request:ty, not_found: $not_found:ty, forbidden: $forbidden:ty, unsupported_media_type: $unsupported_media_type:ty, payload_too_large: $payload_too_large:ty, not_implemented: $not_implemented:ty, | // empty ) => { $crate::http::error::Error }; ( @internal bad_request: $_:ty, not_found: $not_found:ty, forbidden: $forbidden:ty, unsupported_media_type: $unsupported_media_type:ty, payload_too_large: $payload_too_large:ty, not_implemented: $not_implemented:ty, | bad_request: $bad_request:ty, $($rest:tt)* ) => { nord_core::ApiError!( @internal bad_request: $bad_request, not_found: $not_found, forbidden: $forbidden, unsupported_media_type: $unsupported_media_type, payload_too_large: $payload_too_large, not_implemented: $not_implemented, | $($rest)* ) };

.... crazy a lot of repeated code of recursive tt muncher

```

error can be custom:

``` #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] pub struct AcceptedMediaType { pub expected: String, }

impl ExpectedMimeType for AcceptedMediaType { fn expected(&self) -> &str { &self.expected } }

impl AcceptedMediaType { pub fn new(value: headers::ContentType) -> Self { Self { expected: value.to_string(), } } } ```

each method has its own error as needed.

openapi integration ```

impl aide::OperationOutput for Error { type Inner = Self;

    fn inferred_responses(
        cx: &mut aide::generate::GenContext,
        op: &mut aide::openapi::Operation,
    ) -> Vec, aide::openapi::Response)> {
        [
            ::operation_response(cx, op)
                .map(|x| (Some(400), x)),
              ....
            ::operation_response(cx, op).map(
                |mut x| {
                    use aide::openapi::{
                        Header, ParameterSchemaOrContent, ReferenceOr, SchemaObject,
                    };
                    let header = Header {
                        description: Some("Expected request media type".into()),
                        style: Default::default(),
                        required: true,
                        deprecated: None,
                        format: ParameterSchemaOrContent::Schema(SchemaObject {
                            json_schema: schemars::schema::Schema::Object(
                                schemars::schema_for!(String).schema,
                            ),
                            external_docs: None,
                            example: None,
                        }),
                        example: Some(serde_json::json!(
                            mime::APPLICATION_OCTET_STREAM.to_string()
                        )),
                        examples: Default::default(),
                        extensions: Default::default(),
                    };
                    x.headers
                        .insert(header::ACCEPT.to_string(), ReferenceOr::Item(header));
                    (Some(415), x)
                },
            ),
            ...
            ::operation_response(cx, op)
                .map(|x| (Some(501), x)),
        ]
        .into_iter()
        .flatten()
        .collect()
    }
} ```

user vs internal errors - tracing:

``` impl IntoResponse for Error where ST: StatusTypes, { fn into_response(self) -> axum::response::Response { let status = self.status_code(); match self { Self::Internal(error) => { let error = &error as &dyn std::error::Error; tracing::error!(error, "internal error during http request"); (status, Json("INTERNAL SERVER ERROR")).into_response() } Self::Forbidden(e) => (status, Json(e)).into_response(), Self::UnsupportedMediaType(e) => { let value = HeaderValue::from_str(e.expected()); let mut resp = (status, Json(e)).into_response(); if let Ok(value) = value { resp.headers_mut().insert(header::ACCEPT, value); } resp } Self::PayloadTooLarge(e) => (status, Json(e)).into_response(), ... } } }

```

and types sugar ``` mod typelevel {

    /// No client error defined; this type can't be constructed.
    #[derive(Debug, Serialize, Deserialize)]
    pub enum Infallible {}

    impl ExpectedMimeType for Infallible {
        fn expected(&self) -> &str {
            unreachable!("Infallible")
        }
    }

    pub trait ExpectedMimeType {
        fn expected(&self) -> &str;
    }

    pub trait StatusTypes {
        type BadRequest: serde::Serialize + OperationOutputInternal;
        type UnsupportedMediaType: serde::Serialize + OperationOutputInternal + ExpectedMimeType;
        ...
    }

    impl StatusTypes for () {
        type BadRequest = Infallible;
        type NotFound = Infallible;
... }

    impl StatusTypes for Infallible {
        type BadRequest = Infallible;
        type NotFound = Infallible;
.. }

    impl StatusTypes
        for (
            BadRequest,
            NotFound,
            Forbidden,
            UnsupportedMediaType,
            PayloadTooLarge,
            NotImplemented,
        )
    {
        type BadRequest = BadRequest;
        type NotFound = NotFound;
        type Forbidden = Forbidden;
        type UnsupportedMediaType = UnsupportedMediaType;
        type PayloadTooLarge = PayloadTooLarge;
        type NotImplemented = NotImplemented;
    }
... }

use typelevel::;

#[derive(Debug)] pub enum Error { Internal(Box), BadRequest(ST::BadRequest), ... }

impl std::fmt::Display for Infallible { fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result { match *self {} } }

impl Error { pub fn internal(value: impl std::error::Error + Send + Sync + 'static) -> Self { Self::Internal(Box::new(value)) }

    pub fn bad_request(value: ST::BadRequest) -> Self {
        Self::BadRequest(value)
    }

    pub fn not_found(value: ST::NotFound) -> Self {
        Self::NotFound(value)
    }

    pub fn forbidden(value: ST::Forbidden) -> Self {
        Self::Forbidden(value)
    }
```
Post reply on HN