@@ -89,7 +89,10 @@ void validHeader() throws Exception {
8989 VerificationResponse vr = new VerificationResponse ();
9090 vr .isValid = true ;
9191 when (fac .verify (eq (header ), any ())).thenReturn (vr );
92-
92+
93+ // handler returns 200 OK
94+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
95+
9396 // settlement succeeds
9497 SettlementResponse sr = new SettlementResponse ();
9598 sr .success = true ;
@@ -236,16 +239,19 @@ void settlementException() throws Exception {
236239 VerificationResponse vr = new VerificationResponse ();
237240 vr .isValid = true ;
238241 when (fac .verify (eq (header ), any ())).thenReturn (vr );
239-
242+
243+ // handler returns 200 OK
244+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
245+
240246 // But settlement throws exception (should return 402)
241247 doThrow (new IOException ("Network error" )).when (fac ).settle (any (), any ());
242-
248+
243249 filter .doFilter (req , resp , chain );
244-
250+
245251 // Request should be processed, but then settlement failure should return 402
246252 verify (chain ).doFilter (req , resp );
247253 verify (resp ).setStatus (HttpServletResponse .SC_PAYMENT_REQUIRED );
248-
254+
249255 // Verify and settle were both called
250256 verify (fac ).verify (eq (header ), any ());
251257 verify (fac ).settle (eq (header ), any ());
@@ -269,19 +275,22 @@ void settlementFailure() throws Exception {
269275 VerificationResponse vr = new VerificationResponse ();
270276 vr .isValid = true ;
271277 when (fac .verify (eq (header ), any ())).thenReturn (vr );
272-
278+
279+ // handler returns 200 OK
280+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
281+
273282 // Settlement fails (facilitator returns success=false)
274283 SettlementResponse sr = new SettlementResponse ();
275284 sr .success = false ;
276285 sr .error = "insufficient balance" ;
277286 when (fac .settle (eq (header ), any ())).thenReturn (sr );
278-
287+
279288 filter .doFilter (req , resp , chain );
280-
289+
281290 // Request should be processed, but then settlement failure should return 402
282291 verify (chain ).doFilter (req , resp );
283292 verify (resp ).setStatus (HttpServletResponse .SC_PAYMENT_REQUIRED );
284-
293+
285294 // Verify and settle were both called
286295 verify (fac ).verify (eq (header ), any ());
287296 verify (fac ).settle (eq (header ), any ());
@@ -326,25 +335,28 @@ void payerExtractedFromPaymentPayload() throws Exception {
326335
327336 String header = p .toHeader ();
328337 when (req .getHeader ("X-PAYMENT" )).thenReturn (header );
329-
338+
330339 // Verification succeeds
331340 VerificationResponse vr = new VerificationResponse ();
332341 vr .isValid = true ;
333342 when (fac .verify (eq (header ), any ())).thenReturn (vr );
334-
335- // Settlement succeeds
343+
344+ // handler returns 200 OK
345+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
346+
347+ // Settlement succeeds
336348 SettlementResponse sr = new SettlementResponse ();
337349 sr .success = true ;
338350 sr .txHash = "0xabcdef1234567890" ;
339351 sr .networkId = "base-sepolia" ;
340352 when (fac .settle (eq (header ), any ())).thenReturn (sr );
341-
353+
342354 filter .doFilter (req , resp , chain );
343-
355+
344356 // Verify request was processed successfully
345357 verify (chain ).doFilter (req , resp );
346358 verify (resp , never ()).setStatus (HttpServletResponse .SC_PAYMENT_REQUIRED );
347-
359+
348360 // Verify X-PAYMENT-RESPONSE header was set
349361 verify (resp ).setHeader (eq ("X-PAYMENT-RESPONSE" ), any ());
350362 verify (resp ).setHeader (eq ("Access-Control-Expose-Headers" ), eq ("X-PAYMENT-RESPONSE" ));
@@ -378,29 +390,32 @@ void payerExtractionWithMissingAuthorization() throws Exception {
378390
379391 String header = p .toHeader ();
380392 when (req .getHeader ("X-PAYMENT" )).thenReturn (header );
381-
393+
382394 // Verification succeeds
383395 VerificationResponse vr = new VerificationResponse ();
384396 vr .isValid = true ;
385397 when (fac .verify (eq (header ), any ())).thenReturn (vr );
386-
387- // Settlement succeeds
398+
399+ // handler returns 200 OK
400+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
401+
402+ // Settlement succeeds
388403 SettlementResponse sr = new SettlementResponse ();
389404 sr .success = true ;
390405 sr .txHash = "0xabcdef1234567890" ;
391406 sr .networkId = "base-sepolia" ;
392407 when (fac .settle (eq (header ), any ())).thenReturn (sr );
393-
408+
394409 filter .doFilter (req , resp , chain );
395-
410+
396411 // Verify request was processed successfully
397412 verify (chain ).doFilter (req , resp );
398413 verify (resp , never ()).setStatus (HttpServletResponse .SC_PAYMENT_REQUIRED );
399-
414+
400415 // Capture the settlement response header
401416 org .mockito .ArgumentCaptor <String > headerCaptor = org .mockito .ArgumentCaptor .forClass (String .class );
402417 verify (resp ).setHeader (eq ("X-PAYMENT-RESPONSE" ), headerCaptor .capture ());
403-
418+
404419 // Decode and verify the settlement response has null payer
405420 String base64Header = headerCaptor .getValue ();
406421 String jsonString = new String (Base64 .getDecoder ().decode (base64Header ));
@@ -427,25 +442,28 @@ void payerExtractionWithMalformedAuthorization() throws Exception {
427442
428443 String header = p .toHeader ();
429444 when (req .getHeader ("X-PAYMENT" )).thenReturn (header );
430-
445+
431446 // Verification succeeds
432447 VerificationResponse vr = new VerificationResponse ();
433448 vr .isValid = true ;
434449 when (fac .verify (eq (header ), any ())).thenReturn (vr );
435-
436- // Settlement succeeds
450+
451+ // handler returns 200 OK
452+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_OK );
453+
454+ // Settlement succeeds
437455 SettlementResponse sr = new SettlementResponse ();
438456 sr .success = true ;
439457 sr .txHash = "0xabcdef1234567890" ;
440458 sr .networkId = "base-sepolia" ;
441459 when (fac .settle (eq (header ), any ())).thenReturn (sr );
442-
460+
443461 filter .doFilter (req , resp , chain );
444-
462+
445463 // Verify request was processed successfully (payer extraction failure should not break processing)
446464 verify (chain ).doFilter (req , resp );
447465 verify (resp , never ()).setStatus (HttpServletResponse .SC_PAYMENT_REQUIRED );
448-
466+
449467 // Capture the settlement response header
450468 org .mockito .ArgumentCaptor <String > headerCaptor = org .mockito .ArgumentCaptor .forClass (String .class );
451469 verify (resp ).setHeader (eq ("X-PAYMENT-RESPONSE" ), headerCaptor .capture ());
@@ -459,6 +477,68 @@ void payerExtractionWithMalformedAuthorization() throws Exception {
459477 "Settlement response should contain null payer when authorization malformed: " + jsonString );
460478 }
461479
480+ /* ------------ error response skips settlement ------------------------- */
481+ @ Test
482+ void errorResponseSkipsSettlement () throws Exception {
483+ when (req .getRequestURI ()).thenReturn ("/private" );
484+
485+ // Create a valid header
486+ PaymentPayload p = new PaymentPayload ();
487+ p .x402Version = 1 ;
488+ p .scheme = "exact" ;
489+ p .network = "base-sepolia" ;
490+ p .payload = Map .of ("resource" , "/private" );
491+ String header = p .toHeader ();
492+ when (req .getHeader ("X-PAYMENT" )).thenReturn (header );
493+
494+ // Verification succeeds
495+ VerificationResponse vr = new VerificationResponse ();
496+ vr .isValid = true ;
497+ when (fac .verify (eq (header ), any ())).thenReturn (vr );
498+
499+ // Simulate handler returning 500 error
500+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_INTERNAL_SERVER_ERROR );
501+
502+ filter .doFilter (req , resp , chain );
503+
504+ // Handler should be called
505+ verify (chain ).doFilter (req , resp );
506+
507+ // But settle should NOT be called for error responses
508+ verify (fac , never ()).settle (any (), any ());
509+ }
510+
511+ /* ------------ 4xx response skips settlement ---------------------------- */
512+ @ Test
513+ void clientErrorResponseSkipsSettlement () throws Exception {
514+ when (req .getRequestURI ()).thenReturn ("/private" );
515+
516+ // Create a valid header
517+ PaymentPayload p = new PaymentPayload ();
518+ p .x402Version = 1 ;
519+ p .scheme = "exact" ;
520+ p .network = "base-sepolia" ;
521+ p .payload = Map .of ("resource" , "/private" );
522+ String header = p .toHeader ();
523+ when (req .getHeader ("X-PAYMENT" )).thenReturn (header );
524+
525+ // Verification succeeds
526+ VerificationResponse vr = new VerificationResponse ();
527+ vr .isValid = true ;
528+ when (fac .verify (eq (header ), any ())).thenReturn (vr );
529+
530+ // Simulate handler returning 404 error
531+ when (resp .getStatus ()).thenReturn (HttpServletResponse .SC_NOT_FOUND );
532+
533+ filter .doFilter (req , resp , chain );
534+
535+ // Handler should be called
536+ verify (chain ).doFilter (req , resp );
537+
538+ // But settle should NOT be called for 4xx responses
539+ verify (fac , never ()).settle (any (), any ());
540+ }
541+
462542 /* ------------ facilitator IOException returns 500 --------------------- */
463543 @ Test
464544 void facilitatorIOExceptionReturns500 () throws Exception {
0 commit comments