Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ public void doFilter(ServletRequest req,
chain.doFilter(req, res);

/* -------- settlement (return errors to user) ------------- */
// Don't settle if response failed (4xx/5xx status codes)
if (response.getStatus() >= 400) {
return;
}

try {
SettlementResponse sr = facilitator.settle(header, buildRequirements(path));
if (sr == null || !sr.success) {
Expand Down
136 changes: 108 additions & 28 deletions java/src/test/java/com/coinbase/x402/server/PaymentFilterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ void validHeader() throws Exception {
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);


// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// settlement succeeds
SettlementResponse sr = new SettlementResponse();
sr.success = true;
Expand Down Expand Up @@ -236,16 +239,19 @@ void settlementException() throws Exception {
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);


// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// But settlement throws exception (should return 402)
doThrow(new IOException("Network error")).when(fac).settle(any(), any());

filter.doFilter(req, resp, chain);

// Request should be processed, but then settlement failure should return 402
verify(chain).doFilter(req, resp);
verify(resp).setStatus(HttpServletResponse.SC_PAYMENT_REQUIRED);

// Verify and settle were both called
verify(fac).verify(eq(header), any());
verify(fac).settle(eq(header), any());
Expand All @@ -269,19 +275,22 @@ void settlementFailure() throws Exception {
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);


// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// Settlement fails (facilitator returns success=false)
SettlementResponse sr = new SettlementResponse();
sr.success = false;
sr.error = "insufficient balance";
when(fac.settle(eq(header), any())).thenReturn(sr);

filter.doFilter(req, resp, chain);

// Request should be processed, but then settlement failure should return 402
verify(chain).doFilter(req, resp);
verify(resp).setStatus(HttpServletResponse.SC_PAYMENT_REQUIRED);

// Verify and settle were both called
verify(fac).verify(eq(header), any());
verify(fac).settle(eq(header), any());
Expand Down Expand Up @@ -326,25 +335,28 @@ void payerExtractedFromPaymentPayload() throws Exception {

String header = p.toHeader();
when(req.getHeader("X-PAYMENT")).thenReturn(header);

// Verification succeeds
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);

// Settlement succeeds

// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// Settlement succeeds
SettlementResponse sr = new SettlementResponse();
sr.success = true;
sr.txHash = "0xabcdef1234567890";
sr.networkId = "base-sepolia";
when(fac.settle(eq(header), any())).thenReturn(sr);

filter.doFilter(req, resp, chain);

// Verify request was processed successfully
verify(chain).doFilter(req, resp);
verify(resp, never()).setStatus(HttpServletResponse.SC_PAYMENT_REQUIRED);

// Verify X-PAYMENT-RESPONSE header was set
verify(resp).setHeader(eq("X-PAYMENT-RESPONSE"), any());
verify(resp).setHeader(eq("Access-Control-Expose-Headers"), eq("X-PAYMENT-RESPONSE"));
Expand Down Expand Up @@ -378,29 +390,32 @@ void payerExtractionWithMissingAuthorization() throws Exception {

String header = p.toHeader();
when(req.getHeader("X-PAYMENT")).thenReturn(header);

// Verification succeeds
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);

// Settlement succeeds

// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// Settlement succeeds
SettlementResponse sr = new SettlementResponse();
sr.success = true;
sr.txHash = "0xabcdef1234567890";
sr.networkId = "base-sepolia";
when(fac.settle(eq(header), any())).thenReturn(sr);

filter.doFilter(req, resp, chain);

// Verify request was processed successfully
verify(chain).doFilter(req, resp);
verify(resp, never()).setStatus(HttpServletResponse.SC_PAYMENT_REQUIRED);

// Capture the settlement response header
org.mockito.ArgumentCaptor<String> headerCaptor = org.mockito.ArgumentCaptor.forClass(String.class);
verify(resp).setHeader(eq("X-PAYMENT-RESPONSE"), headerCaptor.capture());

// Decode and verify the settlement response has null payer
String base64Header = headerCaptor.getValue();
String jsonString = new String(Base64.getDecoder().decode(base64Header));
Expand All @@ -427,25 +442,28 @@ void payerExtractionWithMalformedAuthorization() throws Exception {

String header = p.toHeader();
when(req.getHeader("X-PAYMENT")).thenReturn(header);

// Verification succeeds
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);

// Settlement succeeds

// handler returns 200 OK
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_OK);

// Settlement succeeds
SettlementResponse sr = new SettlementResponse();
sr.success = true;
sr.txHash = "0xabcdef1234567890";
sr.networkId = "base-sepolia";
when(fac.settle(eq(header), any())).thenReturn(sr);

filter.doFilter(req, resp, chain);

// Verify request was processed successfully (payer extraction failure should not break processing)
verify(chain).doFilter(req, resp);
verify(resp, never()).setStatus(HttpServletResponse.SC_PAYMENT_REQUIRED);

// Capture the settlement response header
org.mockito.ArgumentCaptor<String> headerCaptor = org.mockito.ArgumentCaptor.forClass(String.class);
verify(resp).setHeader(eq("X-PAYMENT-RESPONSE"), headerCaptor.capture());
Expand All @@ -459,6 +477,68 @@ void payerExtractionWithMalformedAuthorization() throws Exception {
"Settlement response should contain null payer when authorization malformed: " + jsonString);
}

/* ------------ error response skips settlement ------------------------- */
@Test
void errorResponseSkipsSettlement() throws Exception {
when(req.getRequestURI()).thenReturn("/private");

// Create a valid header
PaymentPayload p = new PaymentPayload();
p.x402Version = 1;
p.scheme = "exact";
p.network = "base-sepolia";
p.payload = Map.of("resource", "/private");
String header = p.toHeader();
when(req.getHeader("X-PAYMENT")).thenReturn(header);

// Verification succeeds
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);

// Simulate handler returning 500 error
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

filter.doFilter(req, resp, chain);

// Handler should be called
verify(chain).doFilter(req, resp);

// But settle should NOT be called for error responses
verify(fac, never()).settle(any(), any());
}

/* ------------ 4xx response skips settlement ---------------------------- */
@Test
void clientErrorResponseSkipsSettlement() throws Exception {
when(req.getRequestURI()).thenReturn("/private");

// Create a valid header
PaymentPayload p = new PaymentPayload();
p.x402Version = 1;
p.scheme = "exact";
p.network = "base-sepolia";
p.payload = Map.of("resource", "/private");
String header = p.toHeader();
when(req.getHeader("X-PAYMENT")).thenReturn(header);

// Verification succeeds
VerificationResponse vr = new VerificationResponse();
vr.isValid = true;
when(fac.verify(eq(header), any())).thenReturn(vr);

// Simulate handler returning 404 error
when(resp.getStatus()).thenReturn(HttpServletResponse.SC_NOT_FOUND);

filter.doFilter(req, resp, chain);

// Handler should be called
verify(chain).doFilter(req, resp);

// But settle should NOT be called for 4xx responses
verify(fac, never()).settle(any(), any());
}

/* ------------ facilitator IOException returns 500 --------------------- */
@Test
void facilitatorIOExceptionReturns500() throws Exception {
Expand Down