From 9bca3635a2fca95c3903533433a3268612a0c0f2 Mon Sep 17 00:00:00 2001 From: sahilleth Date: Sat, 28 Feb 2026 12:05:52 +0530 Subject: [PATCH] feat: add statusCode, code, and description to RazorpayException Enables programmatic error handling for API errors (retry logic, rate limiting, branching by error type). Backward compatible. - Add getStatusCode(), getCode(), getDescription() getters - Populate from API error responses in ApiClient - Use optString for optional code/description (OAuth compatibility) - Add serialVersionUID for safe serialization - Add RazorpayExceptionTest and error-path test in PaymentClientTest --- src/main/java/com/razorpay/ApiClient.java | 14 +++-- .../java/com/razorpay/RazorpayException.java | 59 +++++++++++++++++++ .../java/com/razorpay/PaymentClientTest.java | 19 ++++++ .../com/razorpay/RazorpayExceptionTest.java | 48 +++++++++++++++ 4 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/razorpay/RazorpayExceptionTest.java diff --git a/src/main/java/com/razorpay/ApiClient.java b/src/main/java/com/razorpay/ApiClient.java index b9c82ce1..280d5ad6 100755 --- a/src/main/java/com/razorpay/ApiClient.java +++ b/src/main/java/com/razorpay/ApiClient.java @@ -221,20 +221,22 @@ private String getEntity(JSONObject jsonObj, HttpUrl url) { } private void throwException(int statusCode, JSONObject responseJson) throws RazorpayException { - if (responseJson.has(ERROR)) { + if (responseJson != null && responseJson.has(ERROR)) { JSONObject errorResponse = responseJson.getJSONObject(ERROR); - String code = errorResponse.getString(STATUS_CODE); - String description = errorResponse.getString(DESCRIPTION); - throw new RazorpayException(code + ":" + description); + String code = errorResponse.optString(STATUS_CODE, null); + String description = errorResponse.optString(DESCRIPTION, null); + String message = (code != null && description != null) ? code + ":" + description + : (description != null) ? description : "API request failed"; + throw new RazorpayException(message, statusCode, code, description); } - throwServerException(statusCode, responseJson.toString()); + throwServerException(statusCode, responseJson != null ? responseJson.toString() : ""); } private void throwServerException(int statusCode, String responseBody) throws RazorpayException { StringBuilder sb = new StringBuilder(); sb.append("Status Code: ").append(statusCode).append("\n"); sb.append("Server response: ").append(responseBody); - throw new RazorpayException(sb.toString()); + throw new RazorpayException(sb.toString(), statusCode); } private Class getClass(String entity) { diff --git a/src/main/java/com/razorpay/RazorpayException.java b/src/main/java/com/razorpay/RazorpayException.java index f89c7f45..b1bf1fae 100644 --- a/src/main/java/com/razorpay/RazorpayException.java +++ b/src/main/java/com/razorpay/RazorpayException.java @@ -2,20 +2,79 @@ public class RazorpayException extends Exception { + private static final long serialVersionUID = 1L; + + private final Integer statusCode; + private final String code; + private final String description; + public RazorpayException(String message) { super(message); + this.statusCode = null; + this.code = null; + this.description = null; + } + + public RazorpayException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + this.code = null; + this.description = null; + } + + public RazorpayException(String message, int statusCode, String code, String description) { + super(message); + this.statusCode = statusCode; + this.code = code; + this.description = description; } public RazorpayException(String message, Throwable cause) { super(message, cause); + this.statusCode = null; + this.code = null; + this.description = null; } public RazorpayException(Throwable cause) { super(cause); + this.statusCode = null; + this.code = null; + this.description = null; } public RazorpayException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); + this.statusCode = null; + this.code = null; + this.description = null; + } + + /** + * Returns the HTTP status code when the exception was caused by an API error response. + * + * @return the HTTP status code, or null if not available + */ + public Integer getStatusCode() { + return statusCode; + } + + /** + * Returns the API error code (e.g. BAD_REQUEST_ERROR) when present in the error response. + * + * @return the error code, or null if not available + */ + public String getCode() { + return code; + } + + /** + * Returns the API error description when present in the error response. + * + * @return the error description, or null if not available + */ + public String getDescription() { + return description; } } diff --git a/src/test/java/com/razorpay/PaymentClientTest.java b/src/test/java/com/razorpay/PaymentClientTest.java index 0374daa0..a56250df 100644 --- a/src/test/java/com/razorpay/PaymentClientTest.java +++ b/src/test/java/com/razorpay/PaymentClientTest.java @@ -5,6 +5,7 @@ import org.mockito.InjectMocks; import java.io.IOException; +import java.util.Arrays; import java.util.List; import static org.junit.Assert.*; @@ -1000,6 +1001,24 @@ public void expandedDetails() throws RazorpayException { } } + @Test + public void fetchThrowsExceptionWithStatusCodeAndErrorCode() throws IOException { + String errorResponse = "{\"error\":{\"code\":\"BAD_REQUEST_ERROR\"," + + "\"description\":\"Payment not found\"}}"; + mockResponseFromExternalClient(errorResponse); + mockResponseHTTPCodeFromExternalClient(400); + mockURL(Arrays.asList("v1", "payments", PAYMENT_ID)); + + try { + paymentClient.fetch(PAYMENT_ID); + assertTrue("Expected RazorpayException", false); + } catch (RazorpayException e) { + assertEquals(Integer.valueOf(400), e.getStatusCode()); + assertEquals("BAD_REQUEST_ERROR", e.getCode()); + assertEquals("Payment not found", e.getDescription()); + } + } + @Test public void fetchPaymentMethods() throws RazorpayException { String mockedResponseJson = "{\n" + diff --git a/src/test/java/com/razorpay/RazorpayExceptionTest.java b/src/test/java/com/razorpay/RazorpayExceptionTest.java new file mode 100644 index 00000000..cf9bc900 --- /dev/null +++ b/src/test/java/com/razorpay/RazorpayExceptionTest.java @@ -0,0 +1,48 @@ +package com.razorpay; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class RazorpayExceptionTest { + + @Test + public void constructorWithMessage_SetsMessageOnly() { + RazorpayException ex = new RazorpayException("test message"); + assertEquals("test message", ex.getMessage()); + assertNull(ex.getStatusCode()); + assertNull(ex.getCode()); + assertNull(ex.getDescription()); + } + + @Test + public void constructorWithMessageAndStatusCode_SetsBoth() { + RazorpayException ex = new RazorpayException("Server error", 500); + assertEquals("Server error", ex.getMessage()); + assertEquals(Integer.valueOf(500), ex.getStatusCode()); + assertNull(ex.getCode()); + assertNull(ex.getDescription()); + } + + @Test + public void constructorWithFullParams_SetsAllFields() { + RazorpayException ex = new RazorpayException("BAD_REQUEST_ERROR:Invalid payment id", + 400, "BAD_REQUEST_ERROR", "Invalid payment id"); + assertEquals("BAD_REQUEST_ERROR:Invalid payment id", ex.getMessage()); + assertEquals(Integer.valueOf(400), ex.getStatusCode()); + assertEquals("BAD_REQUEST_ERROR", ex.getCode()); + assertEquals("Invalid payment id", ex.getDescription()); + } + + @Test + public void constructorWithCause_PreservesLegacyBehavior() { + Exception cause = new RuntimeException("underlying"); + RazorpayException ex = new RazorpayException("wrapped", cause); + assertEquals("wrapped", ex.getMessage()); + assertEquals(cause, ex.getCause()); + assertNull(ex.getStatusCode()); + assertNull(ex.getCode()); + assertNull(ex.getDescription()); + } +}