Skip to content

Commit a1ee89a

Browse files
committed
align charge error classification with mpp status semantics
1 parent 1e873d4 commit a1ee89a

2 files changed

Lines changed: 69 additions & 35 deletions

File tree

crates/tempo-common/src/error.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,10 @@ pub enum PaymentError {
242242
},
243243
#[error("Payment rejected by server: {reason}")]
244244
PaymentRejected { reason: String, status_code: u16 },
245-
#[error("Paid request failed with upstream application error: {reason}")]
246-
UpstreamApplicationError { reason: String, status_code: u16 },
247-
#[error("Paid request failed with upstream server error: {reason}")]
248-
UpstreamServerError { reason: String, status_code: u16 },
245+
#[error("Payment response failed at application layer: {reason}")]
246+
PaymentResponseApplicationError { reason: String, status_code: u16 },
247+
#[error("Payment response failed at server layer: {reason}")]
248+
PaymentResponseServerError { reason: String, status_code: u16 },
249249
#[error("Transaction reverted: {0}")]
250250
TransactionReverted(String),
251251
#[error("Channel {channel_id} not found on {network}")]
@@ -415,26 +415,26 @@ mod tests {
415415
}
416416

417417
#[test]
418-
fn test_upstream_application_error_display() {
419-
let err = PaymentError::UpstreamApplicationError {
418+
fn test_payment_response_application_error_display() {
419+
let err = PaymentError::PaymentResponseApplicationError {
420420
reason: "missing query parameter".to_string(),
421421
status_code: 400,
422422
};
423423
assert_eq!(
424424
err.to_string(),
425-
"Paid request failed with upstream application error: missing query parameter"
425+
"Payment response failed at application layer: missing query parameter"
426426
);
427427
}
428428

429429
#[test]
430-
fn test_upstream_server_error_display() {
431-
let err = PaymentError::UpstreamServerError {
430+
fn test_payment_response_server_error_display() {
431+
let err = PaymentError::PaymentResponseServerError {
432432
reason: "gateway timeout".to_string(),
433433
status_code: 502,
434434
};
435435
assert_eq!(
436436
err.to_string(),
437-
"Paid request failed with upstream server error: gateway timeout"
437+
"Payment response failed at server layer: gateway timeout"
438438
);
439439
}
440440

crates/tempo-request/src/payment/charge.rs

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ async fn submit_credential(
274274
/// Maximum characters to include in a payment rejection reason.
275275
const MAX_REJECTION_REASON_CHARS: usize = 500;
276276

277-
/// Parse a non-200 response after payment submission into a descriptive error.
277+
/// Parse an error response after credential submission into a descriptive error.
278278
fn parse_payment_rejection(response: &HttpResponse) -> PaymentError {
279279
let raw_reason = match response.body_string() {
280280
Ok(body) if !body.trim().is_empty() => {
@@ -285,17 +285,19 @@ fn parse_payment_rejection(response: &HttpResponse) -> PaymentError {
285285
let reason = sanitize_for_terminal(&raw_reason);
286286

287287
match response.status_code {
288-
500..=599 => PaymentError::UpstreamServerError {
288+
402 => PaymentError::PaymentRejected {
289289
reason,
290290
status_code: response.status_code,
291291
},
292-
400..=499 if !is_retriable_payment_rejection(response) => {
293-
PaymentError::UpstreamApplicationError {
294-
reason,
295-
status_code: response.status_code,
296-
}
297-
}
298-
_ => PaymentError::PaymentRejected {
292+
500..=599 => PaymentError::PaymentResponseServerError {
293+
reason,
294+
status_code: response.status_code,
295+
},
296+
400..=499 => PaymentError::PaymentResponseApplicationError {
297+
reason,
298+
status_code: response.status_code,
299+
},
300+
_ => PaymentError::PaymentResponseServerError {
299301
reason,
300302
status_code: response.status_code,
301303
},
@@ -325,49 +327,75 @@ mod tests {
325327
}
326328

327329
#[test]
328-
fn test_parse_payment_rejection_classifies_upstream_application_error() {
330+
fn test_parse_payment_rejection_classifies_application_response_error() {
329331
let body = br#"{"error":"invalid_request","message":"missing input"}"#;
330332
let resp = HttpResponse::for_test(400, body);
331333
let err = parse_payment_rejection(&resp);
332334
match err {
333-
PaymentError::UpstreamApplicationError {
335+
PaymentError::PaymentResponseApplicationError {
334336
reason,
335337
status_code,
336338
} => {
337339
assert!(reason.contains("invalid_request"));
338340
assert_eq!(status_code, 400);
339341
}
340-
_ => panic!("expected UpstreamApplicationError"),
342+
_ => panic!("expected PaymentResponseApplicationError"),
341343
}
342344
}
343345

344346
#[test]
345-
fn test_parse_payment_rejection_classifies_upstream_server_error() {
347+
fn test_parse_payment_rejection_classifies_server_response_error() {
346348
let body = b"Transaction reverted";
347349
let resp = HttpResponse::for_test(500, body);
348350
let err = parse_payment_rejection(&resp);
349351
match err {
350-
PaymentError::UpstreamServerError {
352+
PaymentError::PaymentResponseServerError {
351353
reason,
352354
status_code,
353355
} => {
354356
assert_eq!(reason, "Transaction reverted");
355357
assert_eq!(status_code, 500);
356358
}
357-
_ => panic!("expected UpstreamServerError"),
359+
_ => panic!("expected PaymentResponseServerError"),
358360
}
359361
}
360362

363+
#[test]
364+
fn test_parse_payment_rejection_classifies_401_as_application_response_error() {
365+
let resp = HttpResponse::for_test(401, b"login required");
366+
let err = parse_payment_rejection(&resp);
367+
assert!(matches!(
368+
err,
369+
PaymentError::PaymentResponseApplicationError {
370+
status_code: 401,
371+
..
372+
}
373+
));
374+
}
375+
376+
#[test]
377+
fn test_parse_payment_rejection_classifies_403_as_application_response_error() {
378+
let resp = HttpResponse::for_test(403, b"policy denied");
379+
let err = parse_payment_rejection(&resp);
380+
assert!(matches!(
381+
err,
382+
PaymentError::PaymentResponseApplicationError {
383+
status_code: 403,
384+
..
385+
}
386+
));
387+
}
388+
361389
#[test]
362390
fn test_parse_payment_rejection_truncated() {
363391
let body = "a".repeat(600);
364392
let resp = HttpResponse::for_test(500, body.as_bytes());
365393
let err = parse_payment_rejection(&resp);
366394
match err {
367-
PaymentError::UpstreamServerError { reason, .. } => {
395+
PaymentError::PaymentResponseServerError { reason, .. } => {
368396
assert_eq!(reason.len(), MAX_REJECTION_REASON_CHARS);
369397
}
370-
_ => panic!("expected UpstreamServerError"),
398+
_ => panic!("expected PaymentResponseServerError"),
371399
}
372400
}
373401

@@ -376,10 +404,10 @@ mod tests {
376404
let resp = HttpResponse::for_test(500, b"");
377405
let err = parse_payment_rejection(&resp);
378406
match err {
379-
PaymentError::UpstreamServerError { reason, .. } => {
407+
PaymentError::PaymentResponseServerError { reason, .. } => {
380408
assert_eq!(reason, "HTTP 500");
381409
}
382-
_ => panic!("expected UpstreamServerError"),
410+
_ => panic!("expected PaymentResponseServerError"),
383411
}
384412
}
385413

@@ -388,10 +416,10 @@ mod tests {
388416
let resp = HttpResponse::for_test(503, b" \n\t ");
389417
let err = parse_payment_rejection(&resp);
390418
match err {
391-
PaymentError::UpstreamServerError { reason, .. } => {
419+
PaymentError::PaymentResponseServerError { reason, .. } => {
392420
assert_eq!(reason, "HTTP 503");
393421
}
394-
_ => panic!("expected UpstreamServerError"),
422+
_ => panic!("expected PaymentResponseServerError"),
395423
}
396424
}
397425

@@ -400,10 +428,10 @@ mod tests {
400428
let resp = HttpResponse::for_test(500, &[0xff, 0xfe, 0xfd]);
401429
let err = parse_payment_rejection(&resp);
402430
match err {
403-
PaymentError::UpstreamServerError { reason, .. } => {
431+
PaymentError::PaymentResponseServerError { reason, .. } => {
404432
assert_eq!(reason, "HTTP 500");
405433
}
406-
_ => panic!("expected UpstreamServerError"),
434+
_ => panic!("expected PaymentResponseServerError"),
407435
}
408436
}
409437

@@ -416,13 +444,19 @@ mod tests {
416444
}
417445

418446
#[test]
419-
fn test_parse_payment_rejection_keeps_payment_proof_invalid_as_payment_rejected() {
447+
fn test_parse_payment_rejection_classifies_non_402_payment_wrapper_as_application_error() {
420448
let resp = HttpResponse::for_test(
421449
400,
422450
br#"{"error":"payment_proof_invalid","message":"invalid payment proof"}"#,
423451
);
424452
let err = parse_payment_rejection(&resp);
425-
assert!(matches!(err, PaymentError::PaymentRejected { .. }));
453+
assert!(matches!(
454+
err,
455+
PaymentError::PaymentResponseApplicationError {
456+
status_code: 400,
457+
..
458+
}
459+
));
426460
}
427461

428462
#[test]

0 commit comments

Comments
 (0)