Skip to content

Replyable errors#71

Merged
spacebear21 merged 5 commits intoLtbLightning:mainfrom
spacebear21:replyable-errors
Apr 10, 2025
Merged

Replyable errors#71
spacebear21 merged 5 commits intoLtbLightning:mainfrom
spacebear21:replyable-errors

Conversation

@spacebear21
Copy link
Contributor

@spacebear21 spacebear21 commented Mar 28, 2025

This PR addresses #59, #68, #69, and to a lesser extent #70.

I strove to replace all the opaque error types that were previously struct { msg: String } into the much cleaner and less boilerplate struct WrapperError(#[from] payjoin::InnerError) with #[error(transparent)]. There are a few stragglers that I'd like to address but don't have a clear way forward yet, so I highlighted those in PR comments.

Comment on lines +77 to 82
impl From<String> for ImplementationError {
fn from(value: String) -> Self {
Self(value.into())
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImplementationError can be derived from any String. I believe this is fine and similar to how anyhow works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyhow::Error seems to impl From<E: StdError + Send + Sync + 'static> seems like that's not the same as From but String just qualifies

Copy link
Contributor Author

@spacebear21 spacebear21 Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem (iiuc) is that a blanket implementation for From<E> also covers the case where E = ImplementationError, which results in:

error[E0119]: conflicting implementations of trait `From<ImplementationError>` for type `ImplementationError`
  --> src/receive/error.rs:77:1
   |
77 | / impl<E> From<E> for ImplementationError
78 | | where
79 | |     E: std::error::Error + Send + Sync + 'static,
   | |_________________________________________________^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T> From<T> for T;

I think anyhow gets around this with the anyhow_no_core_error macro but can't find implementation details for the macro.

A potential workaround would be to implement and use a pub fn new<E>(error: E) constructor instead of from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like anyhow_no_core_error is a feature, not a macro. And when that feature is disabled this happens

https://github.com/dtolnay/anyhow/blob/bfb89ef244fa60af17fb844dc3bddf4b62e4ac9f/src/context.rs#L18-L30

I have an odd feeling this will bite us but I'm not sure what to do about it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PjParseError derives from payjoin::Uri::from_str(uri.as_str()) which returns a bitcoin_uri::de::Error<payjoin::PjParseError>>, but bitcoin_uri is an inner dependency of payjoin so we can't implement From for that error type. The current hack turns the error into a string and that string into a PjParseError.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should almost definitely be wrapping bitcoin_uri::de::Error<payjoin::PjParseError>> in a payjoin type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean wrap it in rust-payjoin?

Copy link
Contributor Author

@spacebear21 spacebear21 Mar 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PjNotSupported is also interesting because check_pj_supported returns a Box<bitcoin_uri::Uri<'a>> for the error type, which doesn't implement std::Error. The same string hack is used to wrap this error type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is passable. We'll see when doing integrations.

@spacebear21 spacebear21 requested a review from DanGould March 29, 2025 21:29
It was refactored in
payjoin/rust-payjoin@2041f54
and is now now a simple struct containing a well-known error message.
The receive::Error type was previously overloaded to map address parse
errors. Instead, take a parsed address as parameter and let the
implementer handle parsing/network validation.
receive::Error was previously overloaded to map IntoUrlError. Instead,
match the return signature of Receiver::new by exposing a IntoUrlError
wrapper and returning it as the error type.
@spacebear21 spacebear21 marked this pull request as ready for review March 30, 2025 18:38
Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments but overall looking much better than what we had before

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is passable. We'll see when doing integrations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should almost definitely be wrapping bitcoin_uri::de::Error<payjoin::PjParseError>> in a payjoin type.

Error, ImplementationError, InputContributionError, OutputSubstitutionError, ReplyableError,
SelectionError, SerdeJsonError,
};
use crate::uri::error::IntoUrlError;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't help but wonder if we ought to have BadScheme, ParseError variants in bindings. Although I don't believe further processing beyond display and debug are necessary in the bindings, which suggests we don't need the variants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on leaving it opaque unless we find a need for it.


/// The top-level error type for the payjoin receiver
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessarily and purposefully non_exhaustive and not PartialEq and Eq? Or was this an oversight?

I do think partialEq would require a manual implementation because of the Arc but non_exhaustive seems to work fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non_exhaustive seems unnecessary because we map any unexpected inner type to Error::Unexpected, so there's a catch-all in place of the non_exhaustive pattern.

PartialEq and Eq were removed because of the Arcs. Most of the internal rust-payjoin error types don't implement PartialEq/Eq so I'm not sure we can even implement them manually except with string matching?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But might we change this signature to have another variant if some Unexpected error becomes expected? e.g. with v3? In that case, without non_exhaustive we'd make an API break

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reckon if the inner types were Eq we could compare the (*arctype).clone()s

Comment on lines +39 to +43
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct ReplyableError(#[from] receive::ReplyableError);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lost PartialEq and Eq here too. It seems that non_exhaustive is preserved through the wrapper. I suppose OK if our inner types aren't Eq (they should be) and we can update when they are.

Comment on lines +77 to 82
impl From<String> for ImplementationError {
fn from(value: String) -> Self {
Self(value.into())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyhow::Error seems to impl From<E: StdError + Send + Sync + 'static> seems like that's not the same as From but String just qualifies

Comment on lines +155 to +178

/// Extract an OHTTP Encapsulated HTTP POST request to return
/// a Receiver Error Response
pub fn extract_err_req(
&self,
err: &JsonReply,
ohttp_relay: String,
) -> Result<(Request, ClientResponse), SessionError> {
self.0
.clone()
.extract_err_req(&err.clone().into(), ohttp_relay)
.map(|(req, ctx)| (req.into(), ctx.into()))
.map_err(Into::into)
}

/// Process an OHTTP Encapsulated HTTP POST Error response
/// to ensure it has been posted properly
pub fn process_err_res(
&self,
body: &[u8],
context: &ClientResponse,
) -> Result<(), SessionError> {
self.0.clone().process_err_res(body, context.into()).map_err(Into::into)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a test to make sure this works but that's ok in integration imo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note there's a extract_err_req unit test in rust-payjoin but no tests for process_err_res, we should cover them in integration tests too.

Opaque error types can be majorly simplified by declaring struct
wrappers, then using the #[from] and #[transparent] macros from
thiserror to eliminate boilerplate.
Expose the extract_err_req and process_err_res methods on Receiver to
return replyable errors to the sender.

Expose JsonReply wrapper and relevant From implementations to allow
conversions from bound error types into JSON.
@spacebear21
Copy link
Contributor Author

Added non_exhaustive back to receive::Error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants