# Error Handling and Validation

### Building Robust Handlers with Comprehensive Error Management

[← Back to Understanding XRPL(d) RPC Architecture](/core-dev-bootcamp/module06.md)

***

## Introduction

The difference between a fragile handler and a production-ready one lies in **proper error handling and input validation**. Every RPC handler must anticipate failures—invalid input, missing resources, permission issues, and unexpected edge cases—and respond with clear, actionable error messages.

In this section, you'll learn the complete error handling framework used throughout Rippled, including standard error codes, HTTP status mapping, input validation patterns, and strategies for protecting sensitive data while providing useful debugging information.

***

## RPC Error Codes

Rippled defines a comprehensive set of error codes for different failure scenarios:

### Standard Error Codes

**Source Location**: `src/xrpl/protocol/ErrorCodes.h`

```cpp
// Parameter validation errors
rpcINVALID_PARAMS          // Invalid or missing parameters
rpcBAD_SYNTAX              // Malformed request structure
rpcINVALID_API_VERSION     // Requested API version not supported

// Authentication/Permission errors
rpcNO_PERMISSION           // Insufficient role for operation
rpcFORBIDDEN               // Client IP blacklisted
rpcBAD_AUTH_MASTER         // Master key authentication failed
rpcBAD_AUTH_TOKEN          // Token authentication failed

// Ledger-related errors
rpcNO_CURRENT              // No current (open) ledger available
rpcNO_CLOSED               // No closed (validated) ledger available
rpcLGR_NOT_FOUND           // Specified ledger not found
rpcLGR_IDXS_NOTFND         // Ledger indices not found
rpcLGR_INDEX_BOUNDS        // Ledger index out of valid range

// Account-related errors
rpcACT_NOT_FOUND           // Account not found in ledger
rpcACT_MALFORMED           // Account address malformed
rpcDUPLICATE               // Duplicate account in request

// Transaction-related errors
rpcTXN_NOT_FOUND           // Transaction not found
rpcTXN_FAILED              // Transaction validation failed
rpcMASTER_DISABLED         // Master key disabled on account
rpcINSUFFICIENT_FUNDS      // Insufficient funds for operation

// Network/Server errors
rpcNO_NETWORK              // Node not connected to network
rpcCOMMAND_UNIMPLEMENTED   // Command not implemented
rpcUNKNOWN_COMMAND         // Unknown RPC command
rpcINTERNAL                // Internal server error
rpcSLOW_DOWN               // Rate limited - too many requests
```

### Complete Error Code List

For a comprehensive list of all error codes and their meanings:

```cpp
enum RippleErrorCode : int {
    rpcUNKNOWN_COMMAND = -32600,
    rpcINVALID_PARAMS = -32602,
    rpcINTERNAL = -32603,
    rpcNO_CURRENT = 20,
    rpcNO_CLOSED = 21,
    rpcACT_NOT_FOUND = 19,
    rpcACT_MALFORMED = 18,
    // ... many more defined
};
```

***

## HTTP Status Code Mapping

RPC errors must map to appropriate HTTP status codes:

### Mapping Strategy

```cpp
int getHTTPStatusCode(RippleErrorCode errorCode)
{
    switch (errorCode) {
        // 400 Bad Request - Client error in request format
        case rpcINVALID_PARAMS:
        case rpcBAD_SYNTAX:
            return 400;

        // 401 Unauthorized - Authentication required
        case rpcBAD_AUTH_MASTER:
        case rpcBAD_AUTH_TOKEN:
            return 401;

        // 403 Forbidden - Client lacks permission
        case rpcNO_PERMISSION:
        case rpcFORBIDDEN:
            return 403;

        // 404 Not Found - Requested resource doesn't exist
        case rpcACT_NOT_FOUND:
        case rpcTXN_NOT_FOUND:
        case rpcLGR_NOT_FOUND:
            return 404;

        // 429 Too Many Requests - Rate limited
        case rpcSLOW_DOWN:
            return 429;

        // 503 Service Unavailable - Server temporarily unable
        case rpcNO_CURRENT:
        case rpcNO_NETWORK:
            return 503;

        // 500 Internal Server Error - Unexpected error
        default:
            return 500;
    }
}
```

### Example HTTP Response

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "result": {
        "status": "error",
        "error": "invalid_params",
        "error_code": -32602,
        "error_message": "Missing required field: 'account'"
    }
}
```

***

## Error Response Formatting

### Standard Error Response Structure

Every error response follows this format:

```json
{
    "result": {
        "status": "error",
        "error": "error_code_name",
        "error_code": -32602,
        "error_message": "Human-readable error description",
        "request": {
            "command": "the_command_that_failed",
            "... ": "request parameters (sanitized)"
        }
    }
}
```

### Building Error Responses in Code

**Source Location**: `src/xrpld/rpc/detail/RPCHelpers.h`

```cpp
// Simple error - just code and name
return rpcError(rpcINVALID_PARAMS);

// Error with custom message
return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");

// Error with additional details
Json::Value error = rpcError(rpcACT_NOT_FOUND);
error["detail"] = "Account was deleted from ledger";
return error;

// Helper function definition
Json::Value rpcError(RippleErrorCode errorCode,
                     std::string const& message = "")
{
    Json::Value result;
    result[jss::status] = jss::error;
    result[jss::error] = RPC::errorMessage(errorCode);
    result[jss::error_code] = (int)errorCode;
    if (!message.empty())
        result[jss::error_message] = message;
    return result;
}
```

***

## Input Validation Patterns

### Validate Required Fields

```cpp
Json::Value doMyHandler(RPC::JsonContext& context)
{
    // Check for required field
    if (!context.params.isMember(jss::account)) {
        return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
    }

    // Validate field type
    if (!context.params[jss::account].isString()) {
        return rpcError(rpcINVALID_PARAMS,
            "'account' must be a string");
    }

    // Validate field not empty
    std::string accountStr = context.params[jss::account].asString();
    if (accountStr.empty()) {
        return rpcError(rpcINVALID_PARAMS,
            "'account' cannot be empty");
    }

    return Json::Value();  // Valid
}
```

### Validate Numeric Ranges

```cpp
// Validate unsigned integer with bounds
if (context.params.isMember("limit")) {
    if (!context.params["limit"].isUInt()) {
        return rpcError(rpcINVALID_PARAMS,
            "'limit' must be a positive integer");
    }

    unsigned int limit = context.params["limit"].asUInt();

    // Check bounds
    if (limit < 1 || limit > 1000) {
        return rpcError(rpcINVALID_PARAMS,
            "'limit' must be between 1 and 1000");
    }
}

// Validate floating-point ranges
if (context.params.isMember("fee_multiplier")) {
    if (!context.params["fee_multiplier"].isNumeric()) {
        return rpcError(rpcINVALID_PARAMS,
            "'fee_multiplier' must be numeric");
    }

    double multiplier = context.params["fee_multiplier"].asDouble();

    if (multiplier < 0.1 || multiplier > 1000.0) {
        return rpcError(rpcINVALID_PARAMS,
            "'fee_multiplier' must be between 0.1 and 1000");
    }
}
```

### Validate Addresses and Identifiers

```cpp
// Parse and validate account address
std::string accountStr = context.params[jss::account].asString();
auto account = parseBase58<AccountID>(accountStr);

if (!account) {
    return rpcError(rpcACT_MALFORMED,
        "Invalid account address format");
}

// Parse and validate transaction hash
std::string txHashStr = context.params["tx_hash"].asString();
auto txHash = from_hex_string<Hash256>(txHashStr);

if (!txHash) {
    return rpcError(rpcINVALID_PARAMS,
        "Invalid transaction hash format");
}

// Parse and validate currency code
std::string currencyStr = context.params["currency"].asString();
auto currency = to_currency(currencyStr);

if (!currency) {
    return rpcError(rpcINVALID_PARAMS,
        "Invalid currency code");
}
```

### Validate Enum/Choice Parameters

```cpp
std::string command = context.params[jss::command].asString();

static constexpr std::array<std::string_view, 3> validCommands = {
    "buy", "sell", "cancel"
};

if (std::find(validCommands.begin(), validCommands.end(), command)
    == validCommands.end())
{
    return rpcError(rpcINVALID_PARAMS,
        "command must be 'buy', 'sell', or 'cancel'");
}
```

### Validate Optional Fields

```cpp
// Optional field with default
unsigned int ledgerIndex = 0;
if (context.params.isMember(jss::ledger_index)) {
    if (context.params[jss::ledger_index].isString()) {
        // Special values like "current", "validated"
        std::string indexStr = context.params[jss::ledger_index].asString();
        if (indexStr != "current" && indexStr != "validated") {
            return rpcError(rpcINVALID_PARAMS,
                "ledger_index must be numeric or 'current'/'validated'");
        }
    } else if (context.params[jss::ledger_index].isUInt()) {
        ledgerIndex = context.params[jss::ledger_index].asUInt();
    } else {
        return rpcError(rpcINVALID_PARAMS,
            "ledger_index must be numeric or string");
    }
}
```

***

## Sensitive Data Masking

### Protect Private Keys and Secrets

```cpp
// NEVER expose private keys in responses
Json::Value response;

// Bad: Never do this
// response["private_key"] = account.getPrivateKey();

// Good: Omit sensitive data entirely
response[jss::account] = to_string(accountID);
response[jss::public_key] = to_string(publicKey);
```

### Sanitize Error Messages

```cpp
// Bad: Leaks information about internal structure
if (database.query(accountID) == nullptr) {
    return rpcError(rpcACT_NOT_FOUND,
        "SELECT * FROM accounts WHERE id = " + std::to_string(accountID)
        + " returned no rows");
}

// Good: Hide implementation details
if (database.query(accountID) == nullptr) {
    return rpcError(rpcACT_NOT_FOUND,
        "Account not found");
}
```

### Mask Sensitive Request Data

```cpp
Json::Value getSanitizedRequest(RPC::JsonContext const& context)
{
    Json::Value sanitized = context.params;

    // Remove sensitive fields from request echo
    if (sanitized.isMember("secret")) {
        sanitized.removeMember("secret");
    }
    if (sanitized.isMember("seed")) {
        sanitized.removeMember("seed");
    }
    if (sanitized.isMember("private_key")) {
        sanitized.removeMember("private_key");
    }

    // Mask sensitive values
    if (sanitized.isMember("password")) {
        sanitized["password"] = "[REDACTED]";
    }

    return sanitized;
}
```

***

## Exception Handling

### Catch Exceptions in Handlers

```cpp
Json::Value doMyHandler(RPC::JsonContext& context)
{
    try {
        // Handler implementation
        // ...
        return result;
    }
    catch (std::invalid_argument const& ex) {
        return rpcError(rpcINVALID_PARAMS,
            "Invalid argument: " + std::string(ex.what()));
    }
    catch (std::runtime_error const& ex) {
        return rpcError(rpcINTERNAL,
            "Operation failed");  // Don't expose internal error
    }
    catch (std::exception const& ex) {
        return rpcError(rpcINTERNAL,
            "Unexpected error occurred");
    }
}
```

### Standard Exception Types

```cpp
// std::invalid_argument - for validation errors
if (value < 0) {
    throw std::invalid_argument("value must be non-negative");
}

// std::out_of_range - for bounds violations
if (index >= container.size()) {
    throw std::out_of_range("index out of range");
}

// std::logic_error - for logical errors
if (!precondition) {
    throw std::logic_error("precondition not met");
}

// std::runtime_error - for runtime failures
if (!resource.allocate()) {
    throw std::runtime_error("failed to allocate resource");
}
```

***

## Comprehensive Validation Example

Here's a complete example showing all validation patterns:

```cpp
Json::Value doTransferFunds(RPC::JsonContext& context)
{
    // 1. Validate required fields
    for (auto const& field : {"source", "destination", "amount"}) {
        if (!context.params.isMember(field)) {
            return rpcError(rpcINVALID_PARAMS,
                std::string("Missing required field: '") + field + "'");
        }
    }

    // 2. Validate source account
    auto source = parseBase58<AccountID>(
        context.params["source"].asString()
    );
    if (!source) {
        return rpcError(rpcACT_MALFORMED,
            "Invalid source account address");
    }

    // 3. Validate destination account
    auto destination = parseBase58<AccountID>(
        context.params["destination"].asString()
    );
    if (!destination) {
        return rpcError(rpcACT_MALFORMED,
            "Invalid destination account address");
    }

    // 4. Validate source != destination
    if (*source == *destination) {
        return rpcError(rpcINVALID_PARAMS,
            "Source and destination cannot be the same");
    }

    // 5. Validate amount
    STAmount amount;
    if (!amountFromJsonNoThrow(amount, context.params["amount"])) {
        return rpcError(rpcINVALID_PARAMS,
            "Invalid amount format");
    }

    // 6. Validate amount is positive
    if (amount <= 0) {
        return rpcError(rpcINVALID_PARAMS,
            "Amount must be positive");
    }

    // 7. Optional: validate amount bounds
    if (context.params.isMember("max_amount")) {
        STAmount maxAmount;
        if (!amountFromJsonNoThrow(maxAmount,
            context.params["max_amount"]))
        {
            return rpcError(rpcINVALID_PARAMS,
                "Invalid max_amount format");
        }

        if (amount > maxAmount) {
            return rpcError(rpcINVALID_PARAMS,
                "Amount exceeds maximum allowed");
        }
    }

    // 8. Get and validate ledger
    std::shared_ptr<ReadView const> ledger;
    auto const ledgerResult = RPC::lookupLedger(ledger, context);
    if (!ledger) {
        return ledgerResult;
    }

    // 9. Verify source account exists
    auto sleSource = ledger->read(keylet::account(*source));
    if (!sleSource) {
        return rpcError(rpcACT_NOT_FOUND,
            "Source account not found");
    }

    // 10. Check sufficient balance
    STAmount balance = sleSource->getFieldAmount(sfBalance);
    if (balance < amount) {
        return rpcError(rpcINSUFFICIENT_FUNDS,
            "Insufficient funds in source account");
    }

    // All validation passed - proceed with operation
    Json::Value result;
    result[jss::status] = "success";
    result["transaction_id"] = "..."; // Generated transaction ID
    return result;
}
```

***

## Validation Best Practices

### ✅ DO

* **Validate early and often** — Check all inputs before processing
* **Use specific error messages** — Help clients understand what went wrong
* **Validate all numeric bounds** — Prevent overflow, underflow, and resource exhaustion
* **Check account existence** — Before attempting operations
* **Log validation failures** — For security monitoring and debugging
* **Fail fast** — Return errors as soon as validation fails

### ❌ DON'T

* **Trust client input** — Always validate, even if it looks correct
* **Expose internal errors** — Sanitize error messages
* **Allow injection attacks** — Escape or validate all string inputs
* **Leak sensitive data** — Never include secrets in responses
* **Ignore format validation** — Invalid formats can cause crashes
* **Disable validation for "trusted" clients** — All clients need validation

***

## Common Validation Scenarios

### Scenario 1: Currency/Amount Handling

```cpp
// Validate XRP amount (drops)
if (context.params.isMember("drops")) {
    if (!context.params["drops"].isString()) {
        return rpcError(rpcINVALID_PARAMS,
            "'drops' must be a string");
    }

    std::string dropsStr = context.params["drops"].asString();
    auto drops = XRPAmount::from_string_throw(dropsStr);

    if (drops < 0) {
        return rpcError(rpcINVALID_PARAMS,
            "XRP amount cannot be negative");
    }
}

// Validate IOU amount
if (context.params.isMember("amount")) {
    STAmount amount;
    if (!amountFromJsonNoThrow(amount, context.params["amount"])) {
        return rpcError(rpcINVALID_PARAMS,
            "Invalid amount");
    }

    if (!amount.getCurrency().isValid()) {
        return rpcError(rpcINVALID_PARAMS,
            "Invalid currency in amount");
    }
}
```

### Scenario 2: Ledger Index Selection

```cpp
// Validate and get specific ledger
std::shared_ptr<ReadView const> targetLedger;

if (context.params.isMember(jss::ledger_index)) {
    Json::Value const& indexValue = context.params[jss::ledger_index];

    if (indexValue.isString()) {
        std::string index = indexValue.asString();

        if (index == "validated") {
            targetLedger = context.ledgerMaster.getValidatedLedger();
        } else if (index == "current") {
            targetLedger = context.ledgerMaster.getCurrentLedger();
        } else if (index == "closed") {
            targetLedger = context.ledgerMaster.getClosedLedger();
        } else {
            return rpcError(rpcINVALID_PARAMS,
                "ledger_index must be 'validated', 'current', or a number");
        }
    } else if (indexValue.isUInt()) {
        targetLedger = context.ledgerMaster.getLedgerBySeq(
            indexValue.asUInt()
        );
    } else {
        return rpcError(rpcINVALID_PARAMS,
            "ledger_index must be numeric or string");
    }

    if (!targetLedger) {
        return rpcError(rpcLGR_NOT_FOUND,
            "Ledger not found");
    }
}
```

### Scenario 3: Pagination Validation

```cpp
// Validate pagination parameters
unsigned int pageLimit = 20;  // Default
unsigned int pageIndex = 0;   // Default

if (context.params.isMember("limit")) {
    if (!context.params["limit"].isUInt()) {
        return rpcError(rpcINVALID_PARAMS,
            "'limit' must be a positive integer");
    }

    pageLimit = context.params["limit"].asUInt();

    // Enforce maximum limit to prevent DoS
    if (pageLimit < 1 || pageLimit > 1000) {
        return rpcError(rpcINVALID_PARAMS,
            "'limit' must be between 1 and 1000");
    }
}

if (context.params.isMember("marker")) {
    if (!context.params["marker"].isString()) {
        return rpcError(rpcINVALID_PARAMS,
            "'marker' must be a string");
    }

    std::string marker = context.params["marker"].asString();
    // Validate marker format...
}
```

***

### Conclusion

Comprehensive error handling and input validation separate production-quality handlers from fragile prototypes. Rippled's error framework provides specific codes for every failure scenario, proper HTTP status mapping, and patterns for protecting sensitive information while giving clients actionable feedback. By validating inputs early, handling exceptions gracefully, and following the principle of failing fast, handlers become robust against malformed requests, edge cases, and potential attacks. These practices are fundamental for any handler that will face real-world traffic.

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module06/error-handling-validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
