diff --git a/src/main/java/com/razorpay/ApiClient.java b/src/main/java/com/razorpay/ApiClient.java index b9c82ce1..b9697087 100755 --- a/src/main/java/com/razorpay/ApiClient.java +++ b/src/main/java/com/razorpay/ApiClient.java @@ -8,6 +8,7 @@ import org.apache.commons.text.WordUtils; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import okhttp3.HttpUrl; @@ -106,7 +107,9 @@ else if(response.code()==204){ responseJson = new JSONObject(responseBody); } } catch (IOException e) { - throw new RazorpayException(e.getMessage()); + throw new RazorpayException(e.getMessage(), statusCode); + } catch (JSONException e) { + throw new RazorpayException("Unable to parse response: " + e.getMessage(), statusCode); } if (statusCode < STATUS_OK || statusCode >= STATUS_MULTIPLE_CHOICE) { @@ -170,7 +173,9 @@ T processResponse(Response response) throws RazorpayException responseJson = new JSONObject(responseBody); } } catch (IOException e) { - throw new RazorpayException(e.getMessage()); + throw new RazorpayException(e.getMessage(), statusCode); + } catch (JSONException e) { + throw new RazorpayException("Unable to parse response: " + e.getMessage(), statusCode); } if (statusCode >= STATUS_OK && statusCode < STATUS_MULTIPLE_CHOICE) { @@ -195,7 +200,9 @@ ArrayList processCollectionResponse(Response response) responseBody = response.body().string(); responseJson = new JSONObject(responseBody); } catch (IOException e) { - throw new RazorpayException(e.getMessage()); + throw new RazorpayException(e.getMessage(), statusCode); + } catch (JSONException e) { + throw new RazorpayException("Unable to parse response: " + e.getMessage(), statusCode); } String collectionName = null; @@ -221,20 +228,21 @@ 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); + throw new RazorpayException(code + ":" + description, errorResponse, statusCode); } - throwServerException(statusCode, responseJson.toString()); + String bodyForServerException = responseJson != null ? responseJson.toString() : "Invalid or empty response from server"; + throwServerException(statusCode, bodyForServerException); } 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..9843dc4f 100644 --- a/src/main/java/com/razorpay/RazorpayException.java +++ b/src/main/java/com/razorpay/RazorpayException.java @@ -1,21 +1,82 @@ package com.razorpay; +import org.json.JSONObject; + public class RazorpayException extends Exception { + private transient JSONObject errorResponse; + private int statusCode; + public RazorpayException(String message) { super(message); + this.statusCode = 0; + this.errorResponse = null; } public RazorpayException(String message, Throwable cause) { super(message, cause); + this.statusCode = 0; + this.errorResponse = null; } public RazorpayException(Throwable cause) { super(cause); + this.statusCode = 0; + this.errorResponse = null; } public RazorpayException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); + this.statusCode = 0; + this.errorResponse = null; + } + + public RazorpayException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + this.errorResponse = null; + } + + public RazorpayException(String message, JSONObject errorResponse, int statusCode) { + super(message); + this.errorResponse = errorResponse; + this.statusCode = statusCode; + } + + public JSONObject getErrorResponse() { + return errorResponse; + } + + public int getStatusCode() { + return statusCode; + } + + public String getCode() { + return errorResponse != null ? errorResponse.optString("code", null) : null; + } + + public String getDescription() { + return errorResponse != null ? errorResponse.optString("description", null) : null; + } + + public String getField() { + return errorResponse != null ? errorResponse.optString("field", null) : null; + } + + public String getReason() { + return errorResponse != null ? errorResponse.optString("reason", null) : null; + } + + public String getSource() { + return errorResponse != null ? errorResponse.optString("source", null) : null; + } + + public String getStep() { + return errorResponse != null ? errorResponse.optString("step", null) : null; + } + + public JSONObject getMetadata() { + return errorResponse != null ? errorResponse.optJSONObject("metadata") : null; } } diff --git a/src/test/java/com/razorpay/RazorpayExceptionTest.java b/src/test/java/com/razorpay/RazorpayExceptionTest.java new file mode 100644 index 00000000..dba19472 --- /dev/null +++ b/src/test/java/com/razorpay/RazorpayExceptionTest.java @@ -0,0 +1,236 @@ +package com.razorpay; + +import okhttp3.ResponseBody; +import org.json.JSONObject; +import org.junit.Test; +import org.mockito.InjectMocks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RazorpayExceptionTest extends BaseTest { + + @InjectMocks + protected OrderClient orderClient = new OrderClient(TEST_SECRET_KEY); + + private static final String ERROR_RESPONSE_JSON = "{" + + "\"error\":{" + + "\"code\":\"BAD_REQUEST_ERROR\"," + + "\"description\":\"The amount must be at least 100\"," + + "\"source\":\"business\"," + + "\"step\":\"payment_initiation\"," + + "\"reason\":\"input_validation_failed\"," + + "\"metadata\":{" + + "\"payment_id\":\"pay_EDNBKIP31Y4jl8\"," + + "\"order_id\":\"order_DBJKIP31Y4jl8\"" + + "}," + + "\"field\":\"amount\"" + + "}}"; + + private void mockNonJsonResponse(String rawBody) { + ResponseBody rb = mock(ResponseBody.class); + try { + when(rb.string()).thenReturn(rawBody); + } catch (IOException e) { + fail("Mock setup failed"); + } + when(mockedResponse.body()).thenReturn(rb); + } + + @Test + public void testStructuredErrorResponseIsAvailable() { + try { + mockResponseFromExternalClient(ERROR_RESPONSE_JSON); + mockResponseHTTPCodeFromExternalClient(400); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertNotNull(e.getErrorResponse()); + JSONObject error = e.getErrorResponse(); + assertEquals("BAD_REQUEST_ERROR", error.getString("code")); + assertEquals("The amount must be at least 100", error.getString("description")); + assertEquals("business", error.getString("source")); + assertEquals("payment_initiation", error.getString("step")); + assertEquals("input_validation_failed", error.getString("reason")); + assertEquals("amount", error.getString("field")); + assertTrue(error.has("metadata")); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testStatusCodeIsAvailable() { + try { + mockResponseFromExternalClient(ERROR_RESPONSE_JSON); + mockResponseHTTPCodeFromExternalClient(400); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals(400, e.getStatusCode()); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testMessageIsBackwardCompatible() { + try { + mockResponseFromExternalClient(ERROR_RESPONSE_JSON); + mockResponseHTTPCodeFromExternalClient(400); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals("BAD_REQUEST_ERROR:The amount must be at least 100", e.getMessage()); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testServerExceptionHasStatusCode() { + try { + mockResponseFromExternalClient("{\"unrecognized\":\"body\"}"); + mockResponseHTTPCodeFromExternalClient(500); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals(500, e.getStatusCode()); + assertNull(e.getErrorResponse()); + assertTrue(e.getMessage().contains("Status Code: 500")); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testNonJsonResponseDoesNotCrash() { + try { + mockNonJsonResponse("503 Service Unavailable"); + mockResponseHTTPCodeFromExternalClient(503); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals(503, e.getStatusCode()); + assertNull(e.getErrorResponse()); + assertTrue(e.getMessage().contains("Unable to parse response")); + } + } + + @Test + public void testExceptionConstructorsDefaults() { + RazorpayException ex1 = new RazorpayException("test message"); + assertEquals("test message", ex1.getMessage()); + assertEquals(0, ex1.getStatusCode()); + assertNull(ex1.getErrorResponse()); + + RazorpayException ex2 = new RazorpayException("test message", new JSONObject("{\"code\":\"ERR\"}"), 404); + assertEquals("test message", ex2.getMessage()); + assertEquals(404, ex2.getStatusCode()); + assertNotNull(ex2.getErrorResponse()); + assertEquals("ERR", ex2.getErrorResponse().getString("code")); + + RazorpayException ex3 = new RazorpayException("server error", 502); + assertEquals("server error", ex3.getMessage()); + assertEquals(502, ex3.getStatusCode()); + assertNull(ex3.getErrorResponse()); + } + + @Test + public void testExceptionIsSerializable() { + RazorpayException ex = new RazorpayException( + "BAD_REQUEST_ERROR:The amount must be at least 100", + new JSONObject(ERROR_RESPONSE_JSON).getJSONObject("error"), + 400 + ); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ex); + oos.close(); + } catch (IOException e) { + fail("RazorpayException with errorResponse should be serializable: " + e.getMessage()); + } + } + + @Test + public void testExceptionSerializableWithoutErrorResponse() { + RazorpayException ex = new RazorpayException("server error", 502); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ex); + oos.close(); + } catch (IOException e) { + fail("RazorpayException without errorResponse should be serializable: " + e.getMessage()); + } + } + + @Test + public void testSafeGettersWithFullError() { + try { + mockResponseFromExternalClient(ERROR_RESPONSE_JSON); + mockResponseHTTPCodeFromExternalClient(400); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals("BAD_REQUEST_ERROR", e.getCode()); + assertEquals("The amount must be at least 100", e.getDescription()); + assertEquals("amount", e.getField()); + assertEquals("input_validation_failed", e.getReason()); + assertEquals("business", e.getSource()); + assertEquals("payment_initiation", e.getStep()); + assertNotNull(e.getMetadata()); + assertEquals("pay_EDNBKIP31Y4jl8", e.getMetadata().getString("payment_id")); + assertEquals("order_DBJKIP31Y4jl8", e.getMetadata().getString("order_id")); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testSafeGettersWithMissingFieldsReturnNull() { + String authErrorJson = "{\"error\":{\"code\":\"BAD_REQUEST_ERROR\",\"description\":\"Invalid API Key\"}}"; + try { + mockResponseFromExternalClient(authErrorJson); + mockResponseHTTPCodeFromExternalClient(401); + mockURL(Collections.singletonList("orders")); + orderClient.create(new JSONObject("{\"amount\":50,\"currency\":\"INR\"}")); + fail("Expected RazorpayException"); + } catch (RazorpayException e) { + assertEquals("BAD_REQUEST_ERROR", e.getCode()); + assertEquals("Invalid API Key", e.getDescription()); + assertNull(e.getField()); + assertNull(e.getReason()); + assertNull(e.getSource()); + assertNull(e.getStep()); + assertNull(e.getMetadata()); + } catch (IOException e) { + fail("Unexpected IOException"); + } + } + + @Test + public void testSafeGettersReturnNullWhenNoErrorResponse() { + RazorpayException ex = new RazorpayException("network error", 503); + assertNull(ex.getCode()); + assertNull(ex.getDescription()); + assertNull(ex.getField()); + assertNull(ex.getReason()); + assertNull(ex.getSource()); + assertNull(ex.getStep()); + assertNull(ex.getMetadata()); + } +}