# Spring Boot ↔ Node.js API Error Handling Analysis

## Executive Summary

The cryptoments project uses a **declarative REST client pattern** (Axim framework's `@XRestService` / `@XRestAPI`) for Spring Boot to call Node.js services. However, the error handling structure has **critical gaps**:

1. **Node.js returns unstructured JSON error responses** with only `{ error: "message" }` format
2. **Spring catches all errors as generic `XRestException`** without distinguishing error types
3. **Connection failures are not explicitly handled** — rely on RestClient's default behavior
4. **No standardized error codes between Spring and Node.js** — error message strings are used for classification
5. **Limited error context propagation** — Node.js error details are available but not consistently logged
6. **Timeout configuration is asymmetric** — Spring has separate connection/response timeouts; Node.js has no explicit timeout handling

---

## Spring Boot Side: HTTP Clients

### Client Files & Patterns

#### 1. **BlockchainApiClient** (`common/src/main/java/com/cryptoments/common/client/BlockchainApiClient.java`)
- **Type**: Declarative interface with `@XRestService` + `@XRestAPI` annotations
- **Framework**: Axim REST Framework
- **Target**: blockchain-api (Node.js, port 3001)
- **Call Pattern**: Synchronous method calls

```java
@XRestService(value = "blockchain-api", host = "${axim.web-client.services.blockchain-api}")
public interface BlockchainApiClient {
    @XRestAPI(value = "/api/wallet/derive", method = XHttpMethod.POST)
    WalletDeriveResponse deriveWallet(@RequestBody WalletDeriveRequest request);

    @XRestAPI(value = "/api/balance/sync", method = XHttpMethod.POST)
    BalanceSyncResponse syncBalances(@RequestBody BalanceSyncRequest request);

    // ... 20+ other endpoints
}
```

**Key Points**:
- **No try-catch in the interface** — all error handling delegated to Axim framework
- **Methods throw exceptions on failure** — not Optional or result types
- **Configuration**: Host resolved from `${axim.web-client.services.blockchain-api}` (application.yml)
- **Timeout**: `response-timeout: 60` seconds (global Axim setting)

**Endpoints Called**:
- POST `/api/wallet/derive` — HD wallet derivation
- POST `/api/balance/sync` — Balance synchronization
- GET `/api/wallet/approve-status/{address}` — Approve status
- GET `/api/balance/token/{address}` — Token balance
- GET `/api/balance/native/{address}` — Native coin balance
- GET `/api/tx/status/{txHash}` — TX status
- GET `/api/tx/receipt/{txHash}` — TX receipt
- GET `/api/gas/price` — Gas price
- POST `/api/gas/estimate` — Gas estimation
- GET `/api/block/latest` — Latest block
- POST `/api/admin/wallet/create-admin` — Admin wallet creation
- POST `/api/admin/wallet/create-infra` — Infra wallet creation
- POST `/api/admin/contract/register` — Contract registration
- POST `/api/admin/contract/add-relayer` — Add relayer to contract
- POST `/api/admin/contract/remove-relayer` — Remove relayer from contract
- POST `/api/admin/contract/pause` — Pause contract
- POST `/api/admin/contract/unpause` — Unpause contract
- GET `/api/admin/contract/status/{networkId}` — Contract status
- GET `/api/admin/contract/list` — Contract list
- GET `/api/nonce/{address}` — On-chain nonce

---

#### 2. **RelayerApiClient** (`common/src/main/java/com/cryptoments/common/client/RelayerApiClient.java`)
- **Type**: Declarative interface with `@XRestService` + `@XRestAPI` annotations
- **Target**: relayer-api (Node.js, port 3002)

```java
@XRestService(value = "relayer-api", host = "${axim.web-client.services.relayer-api}")
public interface RelayerApiClient {
    @XRestAPI(value = "/api/relayer/status", method = XHttpMethod.GET)
    List<RelayerStatusResponse> getRelayerStatus();

    @XRestAPI(value = "/api/relayer/register", method = XHttpMethod.POST)
    RelayerRegisterResponse registerRelayer(@RequestBody RelayerRegisterRequest request);

    @XRestAPI(value = "/api/relayer/unregister", method = XHttpMethod.POST)
    RelayerUnregisterResponse unregisterRelayer(@RequestBody RelayerUnregisterRequest request);

    // ... more endpoints
}
```

**Endpoints Called**:
- GET `/api/relayer/status` — Get relayer status
- POST `/api/relayer/register` — Register relayer (HD derive + on-chain)
- POST `/api/relayer/unregister` — Unregister relayer
- GET `/api/relayer/list` — List all relayers
- GET `/api/relayer/status/{walletId}` — Single relayer status

---

#### 3. **SchedulerBlockchainApiClient** (`scheduler/src/main/java/com/cryptoments/scheduler/client/SchedulerBlockchainApiClient.java`)
- **Type**: Low-level implementation using Java 11's `HttpClient` (NOT Axim framework)
- **Target**: blockchain-api (Node.js, port 3001)
- **Call Pattern**: Synchronous HTTP request/response

```java
public class SchedulerBlockchainApiClient {
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;

    public TxReceipt getTxReceipt(Long networkId, String txHash) {
        String url = baseUrl + "/api/v1/tx/" + networkId + "/" + txHash + "/receipt";

        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(Duration.ofSeconds(15))
                    .GET()
                    .build();

            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() == 200) {
                return objectMapper.readValue(response.body(), TxReceipt.class);
            } else if (response.statusCode() == 404) {
                return null;  // TX not found
            } else {
                log.warn("blockchain-api 응답 오류: status={}", response.statusCode());
                return null;  // Suppress error
            }
        } catch (Exception e) {
            log.error("blockchain-api 호출 실패: networkId={}, txHash={}, error={}",
                    networkId, txHash, e.getMessage());
            return null;  // Return null on any error
        }
    }
}
```

**Error Handling**:
- **200**: Parse and return TxReceipt
- **404**: Return null
- **Other 4xx/5xx**: Log warning, return null
- **Connection error (Exception)**: Log error, return null
- **Timeout**: Caught by Exception handler, returns null

**Issues**:
- Silently suppresses all errors → no differentiation between "TX pending" vs "network down"
- 15-second timeout separate from Axim configuration
- `null` return value ambiguous (could mean pending or failed)

---

#### 4. **BithumbApiClient** (`scheduler/src/main/java/com/cryptoments/scheduler/client/BithumbApiClient.java`)
- **Type**: Low-level HTTP implementation (external service — not Node.js)
- **Target**: Bithumb public API (KRW market data)

Error handling similar to SchedulerBlockchainApiClient: silently returns null on failure.

---

#### 5. **TelegramBotClient** (`core/src/main/java/com/cryptoments/core/notification/TelegramBotClient.java`)
- **Type**: RestClient-based (Axim framework)
- **Target**: Telegram Bot API (external)

```java
public boolean sendMessage(String botTokenRef, String chatId, String text, String parseMode) {
    // ... validation ...

    try {
        String response = restClient.post()
                .uri(url)
                .header("Content-Type", "application/json")
                .body(request)
                .retrieve()
                .body(String.class);

        log.info("Telegram 발송 성공");
        return true;
    } catch (Exception e) {
        log.error("Telegram 발송 실패: error={}", e.getMessage());
        return false;  // Suppress error
    }
}
```

**Error Handling**:
- Any exception returns `false`
- No differentiation between types of failures
- Error message is logged but not propagated

---

### How Spring Services Use the Clients

#### Service Usage Pattern 1: Direct Exception Mapping (admin-api)

**ContractManagementService.java**:
```java
public ContractRegisterResponse registerContract(ContractRegisterRequest request, Long adminId) {
    try {
        response = blockchainApiClient.registerContract(request);
    } catch (XRestException e) {
        log.error("컨트랙트 등록 실패: networkId={}, error={}",
                  request.getNetworkId(), e.getMessage());
        throw new ExternalServiceException(
            ErrorCodes.BLOCKCHAIN_API_ERROR,
            e.getMessage());
    }
    // ...
    return response;
}
```

**Behavior**:
- Catches `XRestException` (thrown by Axim on HTTP error)
- Wraps in `ExternalServiceException` (HTTP 504 Gateway Timeout)
- Propagates Node.js error message directly
- **Problem**: No distinction between 4xx (bad request) vs 5xx (server error) vs timeout vs connection refused

---

#### Service Usage Pattern 2: Catch-All Exception (admin-api)

**WalletManagementService.java**:
```java
public BalanceSyncResponse syncBalances(Long adminId) {
    try {
        response = blockchainApiClient.syncBalances(
            BalanceSyncRequest.builder().walletAddressIds(walletAddressIds).build());
    } catch (Exception e) {
        log.error("잔액 동기화 실패: {}", e.getMessage(), e);
        throw new ExternalServiceException(
            ErrorCodes.BALANCE_SYNC_FAILED,
            e.getMessage());
    }
    return response;
}
```

**Behavior**:
- Catches generic `Exception` (too broad)
- Loses exception type information
- All errors become HTTP 504

---

#### Service Usage Pattern 3: No Error Handling (core)

**WalletService.java** (deriveWallet call):
```java
private WalletDeriveResponse deriveWallet(Long networkId, String walletType,
                                           Long partnerId, String partnerUserId) {
    HdWallet hdWallet = hdWalletRepository.findByNetworkId(networkId);
    if (hdWallet == null) {
        throw new NotFoundException(ErrorCodes.HD_WALLET_NOT_FOUND);
    }

    // NO TRY-CATCH — Exception from blockchainApiClient propagates up
    return blockchainApiClient.deriveWallet(WalletDeriveRequest.builder()
            .networkId(networkId)
            .hdWalletId(hdWallet.getId())
            .walletType(walletType)
            .partnerId(partnerId)
            .partnerUserId(partnerUserId)
            .build());
}
```

**Behavior**:
- If blockchainApiClient throws exception, it propagates directly
- Caller must handle or let it bubble to global exception handler
- No context added (caller doesn't know which operation failed)

---

### Axim Framework Client Configuration

**application.yml** (admin-api):
```yaml
axim:
  rest:
    debug: false
    client:
      pool-size: 200
      connection-request-timeout: 30        # Seconds
      response-timeout: 60                  # Seconds
  web-client:
    services:
      blockchain-api: ${NODE_BLOCKCHAIN_API_URL:http://localhost:3001}
      relayer-api: ${NODE_RELAYER_API_URL:http://localhost:3002}
```

**Timeout Behavior**:
- **connection-request-timeout: 30s** — Wait for connection from pool or fail
- **response-timeout: 60s** — Wait for response after request sent
- If timeout exceeds: Axim throws `HttpClientErrorException` or similar
- Spring wraps in `XRestException`

---

## Node.js Side: Error Response Format

### blockchain-api Error Responses

**app.ts** (Global error handler):
```typescript
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
    logger.error('Unhandled error', err);
    res.status(500).json({ error: 'Internal Server Error' });
});
```

**Route handlers** (e.g., wallet.ts):
```typescript
router.post('/derive', async (req, res) => {
  try {
    const { networkId, hdWalletId, derivationIndex, walletType, partnerId, partnerUserId } = req.body;

    if (!networkId || !hdWalletId) {
      return res.status(400).json({ error: 'networkId and hdWalletId are required' });
    }

    const result = await deriveWallet({ /* ... */ });
    res.json(result);
  } catch (error: any) {
    logger.error('Derive failed', error);
    res.status(500).json({ error: error.message });  // error.message only!
  }
});
```

**Error Response Format**:
```json
{
  "error": "some error message"
}
```

**Observations**:
- All errors (4xx validation, 5xx server, connection) respond with 5xx-like `{ error: "..." }`
- No error codes (like `ENOENT`, `ECONNREFUSED`)
- No stacktraces or technical details
- Message is `error.message` (JavaScript Error object)
- 400 status used only for obvious validation (missing required fields)
- 500 status used for everything else (including actual bugs)

---

#### Examples of Node.js Error Responses

**Balance Sync Success**:
```json
{
  "results": [
    { "walletAddressId": 1, "balances": [...] },
    { "walletAddressId": 2, "balances": [...] }
  ]
}
```

**Balance Sync Failure (RPC down)**:
```
HTTP/1.1 500 Internal Server Error

{
  "error": "Network request failed: connect ECONNREFUSED 127.0.0.1:8545"
}
```

**Balance Sync Failure (Invalid parameter)**:
```
HTTP/1.1 400 Bad Request

{
  "error": "walletAddressIds array is required"
}
```

**Derive Wallet Failure (RPC error)**:
```
HTTP/1.1 500 Internal Server Error

{
  "error": "Invalid RPC response: -32602 Invalid params"
}
```

---

### relayer-api Error Responses

**Same pattern as blockchain-api**:

**Relayer Register Failure (HD wallet not found)**:
```
HTTP/1.1 404 Not Found

{
  "error": "HD wallet not found"
}
```

**Relayer Register Failure (On-chain TX failed)**:
```
HTTP/1.1 500 Internal Server Error

{
  "error": "addRelayer TX failed: insufficient funds for gas * price + value"
}
```

**Relayer Register Success**:
```json
{
  "walletAddressId": 123,
  "address": "0x...",
  "txHash": "0x..."
}
```

---

## Error Handling Gaps Analysis

### Gap 1: No Distinction Between Error Types

**Spring receives**:
```
HTTP 500 | { error: "Network request failed: connect ECONNREFUSED" }
HTTP 500 | { error: "Invalid RPC response: -32602" }
HTTP 500 | { error: "Insufficient balance for gas" }
HTTP 400 | { error: "Missing required field: networkId" }
```

**Spring's response**: All wrapped in `ExternalServiceException` (HTTP 504)

**Missing**:
- Separate handling for validation errors (should be 400)
- Separate handling for blockchain RPC errors (should be 424 Dependency Failed or 502 Bad Gateway)
- Separate handling for insufficient funds (should be 402 Payment Required or business error)
- Separate handling for connection timeout (should be 504 Gateway Timeout)

---

### Gap 2: Connection Failures Not Explicitly Handled

**When blockchain-api is down**:

Spring (Axim RestClient):
- Throws `HttpClientErrorException` or timeout exception
- Caught as generic `Exception` in service layer
- Wrapped in `ExternalServiceException` (HTTP 504)
- **Problem**: Cannot distinguish "network down" from "API returned 500"

Scheduler (HttpClient):
- Throws `IOException` (connection refused)
- Caught as generic `Exception`
- Returns `null`
- **Problem**: `null` is ambiguous (could mean "not found" or "unreachable")

---

### Gap 3: No Error Context Propagation

Example failure flow:
```
blockchain-api: deriveWallet() throws "Invalid RPC response"
    ↓
Spring HttpClient catches IOException/HttpException
    ↓
Axim wraps in XRestException with status 500
    ↓
Service catches XRestException
    ↓
Service wraps in ExternalServiceException (504)
    ↓
Error message: "Invalid RPC response"
    ↓
Global handler logs: { error: "Invalid RPC response", status: 504 }
```

**Lost context**:
- Original HTTP status from Node.js (was 500)
- Which operation failed (derive? balance? contract?)
- Which parameters triggered the error (networkId? address?)
- Request ID or correlation ID

---

### Gap 4: Timeout Configuration Asymmetry

**Spring (Axim)**:
- `response-timeout: 60` seconds (global)
- `connection-request-timeout: 30` seconds (global)
- Applied to ALL clients (blockchain-api, relayer-api, etc.)

**Node.js**:
- No explicit timeout on incoming HTTP handlers
- Database queries timeout at MySQL connection level (default ~30s)
- RPC calls timeout at ethers.js/TronWeb provider level
- Express doesn't abort request after timeout

**Result**:
- If Node.js RPC call hangs, Spring waits full 60s
- If Node.js DB query hangs, Spring waits full 60s
- No differentiation between "slow" (50-60s) vs "hung"

---

### Gap 5: No Structured Error Codes

Node.js sends raw error messages:
```json
{
  "error": "Network request failed: connect ECONNREFUSED 127.0.0.1:8545"
}
```

Spring must parse message strings:
```typescript
if (e.getMessage().contains("ECONNREFUSED")) {
    // Connection failed
}
if (e.getMessage().contains("Invalid RPC")) {
    // RPC error
}
```

**Problem**: String parsing is fragile and coupled to Node.js error formatting

---

### Gap 6: No Retry Mechanism

Current behavior:
- Spring calls Node.js
- Node.js fails
- Spring returns error immediately
- No retry logic

**When needed**:
- Temporary RPC node failure → should retry
- Transient network hiccup → should retry
- Node.js process restarting → should retry

**Not implemented**.

---

## Summary Table

| Aspect | Axim Client (BlockchainApiClient) | Low-level HTTP (SchedulerBlockchainApiClient) | Telegram Bot |
|--------|---|---|---|
| **HTTP Call Method** | Declarative `@XRestAPI` | `HttpClient.send()` | `RestClient` |
| **Error Exception Type** | `XRestException` | `IOException`, `HttpException` | Generic `Exception` |
| **Error Response Parsing** | Automatic via Axim | Manual via ObjectMapper | String body |
| **Timeout Handling** | Global Axim config (60s) | Explicit (15s) | Global config |
| **4xx/5xx Distinction** | No (all XRestException) | No (all caught, null returned) | No |
| **Error Message Propagation** | Yes (via getMessage()) | Yes (manual logging) | No (only logged) |
| **Retry Logic** | None | None | None |
| **Connection Failure Handling** | Via XRestException | Via IOException catch | Via Exception catch |
| **Log Context** | e.getMessage() only | e.getMessage() only | e.getMessage() only |

---

## Recommendations

### Short-term Fixes
1. **Add structured error codes to Node.js** — Replace `{ error: "string" }` with `{ error: "string", code: "ERR_CODE" }`
2. **Add HTTP status differentiation in Spring** — Catch `HttpClientErrorException` (4xx), `HttpServerErrorException` (5xx) separately
3. **Add request/response logging decorator** — Log request URL, parameters, response status, timing
4. **Add explicit timeout configuration per client** — Not global, but per-service

### Medium-term Improvements
1. **Implement circuit breaker pattern** — Fail fast if Node.js is down repeatedly
2. **Add exponential backoff retry** — For transient failures
3. **Add correlation IDs** — Track request across Spring → Node.js → back
4. **Add metrics collection** — Track error rates, latencies per endpoint
5. **Standardize error response format** — Across all Node.js services

### Long-term Architecture
1. **Consider event-driven instead of synchronous** — For non-critical operations (balance sync, relayer status)
2. **Add dedicated error handler middleware** — In Express for consistent error format
3. **Add OpenAPI/async spec** — Document error responses explicitly
4. **Add integration tests** — Test failure scenarios (Node.js down, timeout, malformed response)
