Skip to content

Commit e8bb1a7

Browse files
authored
Process res error handling (payjoin#1075)
2 parents 615b146 + 08a0161 commit e8bb1a7

6 files changed

Lines changed: 110 additions & 31 deletions

File tree

payjoin-ffi/src/receive/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ pub struct InitializedTransition(
316316
payjoin::receive::v2::SessionEvent,
317317
payjoin::receive::v2::Receiver<payjoin::receive::v2::UncheckedOriginalPayload>,
318318
payjoin::receive::v2::Receiver<payjoin::receive::v2::Initialized>,
319-
payjoin::receive::Error,
319+
payjoin::receive::ProtocolError,
320320
>,
321321
>,
322322
>,

payjoin/src/core/ohttp.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ pub enum DirectoryResponseError {
6060
UnexpectedStatusCode(http::StatusCode),
6161
}
6262

63+
impl DirectoryResponseError {
64+
pub(crate) fn is_fatal(&self) -> bool {
65+
use DirectoryResponseError::*;
66+
67+
match self {
68+
OhttpDecapsulation(_) => true,
69+
InvalidSize(_) => false,
70+
UnexpectedStatusCode(status_code) => status_code.is_client_error(),
71+
}
72+
}
73+
}
74+
6375
impl fmt::Display for DirectoryResponseError {
6476
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6577
use DirectoryResponseError::*;

payjoin/src/core/persist.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ impl<Event, SuccessValue, CurrentState, Err>
1313
MaybeSuccessTransitionWithNoResults(Err(Rejection::fatal(event, error)))
1414
}
1515

16-
#[allow(dead_code)]
1716
#[inline]
1817
pub(crate) fn transient(error: Err) -> Self {
1918
MaybeSuccessTransitionWithNoResults(Err(Rejection::transient(error)))
@@ -64,6 +63,11 @@ impl<Event, NextState, CurrentState, Err>
6463
MaybeFatalTransitionWithNoResults(Ok(AcceptOptionalTransition::NoResults(current_state)))
6564
}
6665

66+
#[inline]
67+
pub(crate) fn transient(error: Err) -> Self {
68+
MaybeFatalTransitionWithNoResults(Err(Rejection::transient(error)))
69+
}
70+
6771
#[inline]
6872
pub(crate) fn success(event: Event, next_state: NextState) -> Self {
6973
MaybeFatalTransitionWithNoResults(Ok(AcceptOptionalTransition::Success(AcceptNextState(
@@ -151,7 +155,7 @@ impl<Event, NextState, Err> MaybeTransientTransition<Event, NextState, Err> {
151155
/// A transition that can result in the completion of a state machine or a transient error
152156
/// Fatal errors cannot occur in this transition.
153157
pub struct MaybeSuccessTransition<Event, SuccessValue, Err>(
154-
Result<AcceptNextState<Event, SuccessValue>, RejectTransient<Err>>,
158+
Result<AcceptNextState<Event, SuccessValue>, Rejection<Event, Err>>,
155159
);
156160

157161
impl<Event, SuccessValue, Err> MaybeSuccessTransition<Event, SuccessValue, Err>
@@ -165,7 +169,12 @@ where
165169

166170
#[inline]
167171
pub(crate) fn transient(error: Err) -> Self {
168-
MaybeSuccessTransition(Err(RejectTransient(error)))
172+
MaybeSuccessTransition(Err(Rejection::transient(error)))
173+
}
174+
175+
#[inline]
176+
pub(crate) fn fatal(event: Event, error: Err) -> Self {
177+
MaybeSuccessTransition(Err(Rejection::fatal(event, error)))
169178
}
170179

171180
pub fn save<P>(
@@ -377,7 +386,10 @@ trait InternalSessionPersister: SessionPersister {
377386
self.close().map_err(InternalPersistedError::Storage)?;
378387
Ok(success_value)
379388
}
380-
Err(RejectTransient(err)) => Err(InternalPersistedError::Transient(err).into()),
389+
Err(Rejection::Transient(RejectTransient(err))) =>
390+
Err(InternalPersistedError::Transient(err).into()),
391+
Err(Rejection::Fatal(RejectFatal(event, err))) =>
392+
Err(self.handle_fatal_reject(RejectFatal(event, err)).into()),
381393
}
382394
}
383395

@@ -798,6 +810,22 @@ mod tests {
798810
MaybeSuccessTransition::transient(InMemoryTestError {}).save(persister)
799811
}),
800812
},
813+
// Fatal error
814+
TestCase {
815+
expected_result: ExpectedResult {
816+
events: vec![InMemoryTestEvent("error event".to_string())],
817+
is_closed: true,
818+
error: Some(InternalPersistedError::Fatal(InMemoryTestError {}).into()),
819+
success: None,
820+
},
821+
test: Box::new(move |persister| {
822+
MaybeSuccessTransition::fatal(
823+
InMemoryTestEvent("error event".to_string()),
824+
InMemoryTestError {},
825+
)
826+
.save(persister)
827+
}),
828+
},
801829
];
802830

803831
for test in test_cases {

payjoin/src/core/receive/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl error::Error for Error {
4747
}
4848
}
4949

50-
/// The protocolerror type for the payjoin receiver, representing failures in
50+
/// The protocol error type for the payjoin receiver, representing failures in
5151
/// the internal protocol operation.
5252
///
5353
/// The error handling is designed to:

payjoin/src/core/receive/v2/mod.rs

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -371,16 +371,35 @@ impl Receiver<Initialized> {
371371
SessionEvent,
372372
Receiver<UncheckedOriginalPayload>,
373373
Receiver<Initialized>,
374-
Error,
374+
ProtocolError,
375375
> {
376376
let current_state = self.clone();
377377
let proposal = match self.inner_process_res(body, context) {
378378
Ok(proposal) => proposal,
379-
Err(e) =>
380-
return MaybeFatalTransitionWithNoResults::fatal(
381-
SessionEvent::SessionInvalid(e.to_string(), None),
382-
e,
383-
),
379+
Err(e) => match e {
380+
// Implementation errors should be unreachable
381+
ProtocolError::V2(ref session_error) => match session_error {
382+
SessionError(InternalSessionError::DirectoryResponse(directory_error)) =>
383+
if directory_error.is_fatal() {
384+
return MaybeFatalTransitionWithNoResults::fatal(
385+
SessionEvent::SessionInvalid(e.to_string(), None),
386+
e,
387+
);
388+
} else {
389+
return MaybeFatalTransitionWithNoResults::transient(e);
390+
},
391+
_ =>
392+
return MaybeFatalTransitionWithNoResults::fatal(
393+
SessionEvent::SessionInvalid(session_error.to_string(), None),
394+
e,
395+
),
396+
},
397+
_ =>
398+
return MaybeFatalTransitionWithNoResults::fatal(
399+
SessionEvent::SessionInvalid(e.to_string(), None),
400+
e,
401+
),
402+
},
384403
};
385404

386405
if let Some((proposal, reply_key)) = proposal {
@@ -403,9 +422,9 @@ impl Receiver<Initialized> {
403422
self,
404423
body: &[u8],
405424
context: ohttp::ClientResponse,
406-
) -> Result<Option<(OriginalPayload, Option<HpkePublicKey>)>, Error> {
425+
) -> Result<Option<(OriginalPayload, Option<HpkePublicKey>)>, ProtocolError> {
407426
let body = match process_get_res(body, context)
408-
.map_err(InternalSessionError::DirectoryResponse)?
427+
.map_err(|e| ProtocolError::V2(InternalSessionError::DirectoryResponse(e).into()))?
409428
{
410429
Some(body) => body,
411430
None => return Ok(None),
@@ -442,12 +461,13 @@ impl Receiver<Initialized> {
442461
fn extract_proposal_from_v2(
443462
self,
444463
response: Vec<u8>,
445-
) -> Result<(OriginalPayload, HpkePublicKey), Error> {
464+
) -> Result<(OriginalPayload, HpkePublicKey), ProtocolError> {
446465
let (payload_bytes, reply_key) =
447-
decrypt_message_a(&response, self.session_context.receiver_key.secret_key().clone())?;
466+
decrypt_message_a(&response, self.session_context.receiver_key.secret_key().clone())
467+
.map_err(|e| ProtocolError::V2(InternalSessionError::Hpke(e).into()))?;
448468
let payload = std::str::from_utf8(&payload_bytes)
449469
.map_err(|e| ProtocolError::OriginalPayload(InternalPayloadError::Utf8(e).into()))?;
450-
self.unchecked_from_payload(payload).map_err(Error::Protocol).map(|p| (p, reply_key))
470+
self.unchecked_from_payload(payload).map(|p| (p, reply_key))
451471
}
452472

453473
fn unchecked_from_payload(self, payload: &str) -> Result<OriginalPayload, ProtocolError> {
@@ -1052,12 +1072,20 @@ impl Receiver<PayjoinProposal> {
10521072
res: &[u8],
10531073
ohttp_context: ohttp::ClientResponse,
10541074
) -> MaybeSuccessTransition<SessionEvent, (), Error> {
1055-
match process_post_res(res, ohttp_context)
1056-
.map_err(|e| InternalSessionError::DirectoryResponse(e).into())
1057-
{
1075+
match process_post_res(res, ohttp_context) {
10581076
Ok(_) =>
10591077
MaybeSuccessTransition::success(SessionEvent::Closed(SessionOutcome::Success), ()),
1060-
Err(e) => MaybeSuccessTransition::transient(e),
1078+
Err(e) =>
1079+
if e.is_fatal() {
1080+
MaybeSuccessTransition::fatal(
1081+
SessionEvent::SessionInvalid(e.to_string(), None),
1082+
InternalSessionError::DirectoryResponse(e).into(),
1083+
)
1084+
} else {
1085+
MaybeSuccessTransition::transient(
1086+
InternalSessionError::DirectoryResponse(e).into(),
1087+
)
1088+
},
10611089
}
10621090
}
10631091
}

payjoin/src/core/send/v2/mod.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,17 @@ impl Sender<WithReplyKey> {
323323
) -> MaybeFatalTransition<SessionEvent, Sender<PollingForProposal>, EncapsulationError> {
324324
match process_post_res(response, post_ctx.ohttp_ctx) {
325325
Ok(()) => {}
326-
Err(e) => {
327-
return MaybeFatalTransition::fatal(
328-
SessionEvent::SessionInvalid(e.to_string()),
329-
InternalEncapsulationError::DirectoryResponse(e).into(),
330-
);
331-
}
326+
Err(e) =>
327+
if e.is_fatal() {
328+
return MaybeFatalTransition::fatal(
329+
SessionEvent::SessionInvalid(e.to_string()),
330+
InternalEncapsulationError::DirectoryResponse(e).into(),
331+
);
332+
} else {
333+
return MaybeFatalTransition::transient(
334+
InternalEncapsulationError::DirectoryResponse(e).into(),
335+
);
336+
},
332337
}
333338

334339
let polling_for_proposal = PollingForProposal {
@@ -473,10 +478,16 @@ impl Sender<PollingForProposal> {
473478
Ok(Some(body)) => body,
474479
Ok(None) => return MaybeSuccessTransitionWithNoResults::no_results(self.clone()),
475480
Err(e) =>
476-
return MaybeSuccessTransitionWithNoResults::fatal(
477-
SessionEvent::SessionInvalid(e.to_string()),
478-
InternalEncapsulationError::DirectoryResponse(e).into(),
479-
),
481+
if e.is_fatal() {
482+
return MaybeSuccessTransitionWithNoResults::fatal(
483+
SessionEvent::SessionInvalid(e.to_string()),
484+
InternalEncapsulationError::DirectoryResponse(e).into(),
485+
);
486+
} else {
487+
return MaybeSuccessTransitionWithNoResults::transient(
488+
InternalEncapsulationError::DirectoryResponse(e).into(),
489+
);
490+
},
480491
};
481492
let psbt = match decrypt_message_b(
482493
&body,

0 commit comments

Comments
 (0)