Skip to content

feat: expose structured API error data in RazorpayException - #361

Merged
ankitdas13 merged 3 commits into
masterfrom
feat/structured-error-exception
Jul 29, 2026
Merged

feat: expose structured API error data in RazorpayException#361
ankitdas13 merged 3 commits into
masterfrom
feat/structured-error-exception

Conversation

@rzp-slash

@rzp-slash rzp-slash Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

The Java SDK discards the Razorpay API's structured error JSON and flattens it to a code:description string. Merchants like Coursera have to parse this string to detect error types — fragile when the description itself contains :.

This has been pending since 2020. Multiple Coursera production incidents in 2026 were harder to diagnose because of this limitation.

Changes

RazorpayException.java

  • Added JSONObject errorResponse and int statusCode fields
  • Added getErrorResponse() — returns the full error JSON object (code, description, source, step, reason, metadata, field)
  • Added getStatusCode() — returns the HTTP status code
  • Added constructors: (String, JSONObject, int) and (String, int)
  • All existing constructors preserved (backward compatible — new fields default to null/0)

ApiClient.java

  • throwException() now passes the full error JSON object + status code to RazorpayException
  • throwServerException() now passes the status code
  • Added JSONException catch in processResponse(), processCollectionResponse(), processDeleteResponse() — handles non-JSON responses (e.g. 503 HTML) gracefully instead of crashing with an unhandled RuntimeException
  • Null-safe responseJson handling in throwException()

Usage

try {
    Order order = razorpayClient.Orders.create(request);
} catch (RazorpayException e) {
    // Backward compatible — message is still "BAD_REQUEST_ERROR:The amount must be at least 100"
    System.out.println(e.getMessage());

    // New — structured error data
    JSONObject error = e.getErrorResponse();
    if (error != null) {
        String code = error.getString("code");           // BAD_REQUEST_ERROR
        String field = error.getString("field");          // amount
        String reason = error.getString("reason");        // input_validation_failed
    }

    // New — HTTP status code
    int statusCode = e.getStatusCode();                   // 400
}

Backward Compatibility

  • The existing string message format (code:description) is unchanged
  • All existing constructors are preserved
  • New fields default to null/0 when using old constructors
  • No breaking changes to any public API

Testing

  • 6 new tests in RazorpayExceptionTest.java covering: structured error response, status code, backward-compatible message, server exception status code, non-JSON response handling, constructor defaults
  • All 176 tests pass (170 existing + 6 new)

Context

  • Slack thread: https://razorpay.slack.com/archives/C0ANVMNU0SH/p1784708746.720379
  • Coursera has been hit by multiple SDK-related pain points — structured error discarding, JSON parse failures on non-JSON responses, and undocumented validation changes
  • The stack trace ApiClient.throwException(ApiClient.java:228) appeared explicitly in Coursera's production error reports

RazorpayException now stores the full error JSON object and HTTP status
code from API responses, accessible via getErrorResponse() and
getStatusCode(). The existing string message (code:description) is
preserved for backward compatibility.

Also adds JSONException guards in processResponse,
processCollectionResponse, and processDeleteResponse to handle non-JSON
responses (e.g. 503 HTML) gracefully instead of crashing with an
unhandled RuntimeException.

Addresses the Coursera ask pending since 2020 — merchants can now detect
specific error types by checking the code field directly instead of
parsing a concatenated string, and access all fields (source, step,
reason, metadata, field) for their error handling logic.

Co-authored-by: ankitdas13 <ankit.das@razorpay.com>
ankitdas13 and others added 2 commits July 28, 2026 15:50
… catches

P0: Mark errorResponse as transient to prevent NotSerializableException
in Spring apps and other serialization contexts. JSONObject is not
Serializable and Exception implements Serializable.

P1: Add safe convenience getters (getCode, getDescription, getField,
getReason, getSource, getStep) that return null for missing fields
instead of throwing JSONException. This prevents crashes when merchants
access optional fields on errors that don't have them (e.g. 401 auth
errors have no 'field' or 'step').

Fix: Pass statusCode to RazorpayException in all 3 IOException catch
blocks in ApiClient for consistency with JSONException catches.

Co-authored-by: ankitdas13 <ankit.das@razorpay.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@manikrazorpay13 manikrazorpay13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@ankitdas13 ankitdas13 added the TestingNotRequired TestingNotRequired label for BVT label Jul 29, 2026
@ankitdas13
ankitdas13 merged commit cc4732f into master Jul 29, 2026
4 checks passed
@ankitdas13 ankitdas13 mentioned this pull request Jul 29, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TestingNotRequired TestingNotRequired label for BVT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants