INTRODUCTION

In Lesson 2.5, you successfully implemented efficient pagination. But what happens when the TPP sends an invalid fromDateTime? Or when the consent has expired? Or when the ASPSP’s database is unreachable?

In open banking, error handling is not a debugging tool—it is a regulatory requirement. The OBIE v4.0 specification dedicates an entire section (4.4) to ErrorResponse structures. The CDR Rules require that error responses contain specific ErrorCode enumerations so that TPPs can programmatically retry or fail. The EBA’s RTS on CSC requires that SCA failures return specific error messages that inform the PSU why the challenge failed.

This lesson teaches you to implement RFC 7807 Problem Details—the standardised error response format adopted by OBIE and CDR. You will learn the exact mapping of HTTP status codes (400 vs. 403 vs. 409 vs. 429) to regulatory scenarios, how to include a traceId for end‑to‑end debugging, and how to implement a circuit breaker that returns 503 Service Unavailable when the backend is degraded.

By the end, your API will return errors that are machine‑parseable, human‑readable, and regulator‑auditable.


LEARNING OBJECTIVES

  1. Implement RFC 7807 Problem Details—constructing the type, title, status, detail, and instance fields, and ensuring compliance with the OBIE v4.0 ErrorResponse schema.

  2. Map regulatory scenarios to correct HTTP status codes—distinguishing between 400 Bad Request (schema validation), 401 Unauthorized (missing/invalid token), 403 Forbidden (insufficient consent), 409 Conflict (idempotency clash), and 429 Too Many Requests (rate limiting).

  3. Design an OBIE‑specific ErrorCode enumeration—categories like FIELD_VALIDATION, CONSENT_INVALID, PAYMENT_REJECTED, SCA_FAILURE, and SERVER_TIMEOUT—and document them in the OpenAPI contract.

  4. Construct a circuit breaker (using Resilience4j or Hystrix) that returns 503 Service Unavailable when the backend dependency fails >50% of requests, and retry with backoff (Lesson 2.3) to recover.

  5. Quantify the latency overhead of error handling—including JSON serialisation of the error object (≤2ms) and logging to the audit trail (≤5ms)—ensuring that errors do not worsen the outage.


PART 1: RFC 7807 PROBLEM DETAILS — The Standard Error Format

1.1 The Structure

RFC 7807 defines a standardised JSON error payload that includes:

 
 
Field Required? Type Purpose
type Yes URI A URI identifying the error type (e.g., https://api.bank.com/errors/field-validation)
title Yes String A short, human‑readable summary (e.g., “Validation Error”)
status Yes Integer HTTP status code (e.g., 400)
detail Optional String Human‑readable explanation (e.g., “The ‘fromDateTime’ field is in the future”)
instance Optional URI URI identifying the specific occurrence (e.g., /payments/ref-123)

OBIE v4.0 ErrorResponse extends this with ErrorCode and ErrorDescription:

json
{
  "type": "https://openbanking.org.uk/errors/field-validation",
  "title": "Validation Error",
  "status": 400,
  "detail": "The 'fromDateTime' field cannot be in the future.",
  "instance": "/payments/ref-123",
  "ErrorCode": "FIELD_VALIDATION_FUTURE_DATE",
  "ErrorDescription": "The 'fromDateTime' parameter is set to a future date."
}

1.2 OpenAPI Declaration for Error Responses

yaml
components:
  schemas:
    ErrorResponse:
      type: object
      required:
        - type
        - title
        - status
        - ErrorCode
        - ErrorDescription
      properties:
        type:
          type: string
          format: uri
          example: "https://openbanking.org.uk/errors/field-validation"
        title:
          type: string
          example: "Validation Error"
        status:
          type: integer
          example: 400
        detail:
          type: string
          example: "The 'fromDateTime' field cannot be in the future."
        instance:
          type: string
          format: uri
          example: "/payments/ref-123"
        ErrorCode:
          type: string
          example: "FIELD_VALIDATION_FUTURE_DATE"
        ErrorDescription:
          type: string
          example: "The 'fromDateTime' parameter is set to a future date."
        traceId:
          type: string
          format: uuid
          description: "Unique identifier for the request (echoed from x-fapi-interaction-id)"

1.3 Error Response Generation in Python

python
import uuid
from flask import jsonify, request

class ErrorResponseBuilder:
    @staticmethod
    def build(error_code, title, detail, status_code, instance=None):
        return jsonify({
            "type": f"https://api.bank.com/errors/{error_code.lower()}",
            "title": title,
            "status": status_code,
            "detail": detail,
            "instance": instance or request.path,
            "ErrorCode": error_code,
            "ErrorDescription": detail,
            "traceId": request.headers.get('x-fapi-interaction-id', str(uuid.uuid4()))
        }), status_code

PART 2: HTTP STATUS CODE MAPPING — Regulatory Scenarios

 
 
Scenario HTTP Status OBIE Error Code Example
Missing mandatory field 400 Bad Request FIELD_VALIDATION_MISSING “The ‘Permissions’ field is required.”
Invalid date format 400 Bad Request FIELD_VALIDATION_FORMAT “The ‘fromDateTime’ must be ISO 8601.”
Missing OAuth2 token 401 Unauthorized AUTH_TOKEN_MISSING “Bearer token is required.”
Invalid OAuth2 token 401 Unauthorized AUTH_TOKEN_INVALID “Token has expired or is malformed.”
Insufficient consent scope 403 Forbidden CONSENT_INSUFFICIENT_SCOPE “Consent does not include ‘transactions:read’.”
Consent revoked 403 Forbidden CONSENT_REVOKED “Consent ct-123 has been revoked.”
Consent expired 403 Forbidden CONSENT_EXPIRED “Consent ct-123 expired on 2026-08-01.”
Idempotency conflict 409 Conflict IDEMPOTENCY_CONFLICT “Key ‘pay-001’ used with different payload.”
Rate limit exceeded 429 Too Many Requests RATE_LIMIT_EXCEEDED “25 RPS limit exceeded. Retry after 3s.”
SCA challenge failed 400 Bad Request SCA_FAILURE_INVALID_OTP “The OTP code is incorrect.”
Backend timeout 504 Gateway Timeout BACKEND_TIMEOUT “The ledger system did not respond in time.”
General server error 500 Internal Server Error SERVER_INTERNAL_ERROR “Unexpected error. Trace ID: abc-123.”

2.1 Handling 401 vs. 403

  • 401 Unauthorized: The TPP did not provide valid credentials (missing or invalid JWT). The TPP should retry with a new token.

  • 403 Forbidden: The TPP is authenticated, but does not have permission (consent revoked, expired, or insufficient scope). The TPP must prompt the PSU to re‑authorise.

2.2 Handling 409 Conflict

As detailed in Lesson 2.3, the 409 response must return the Location of the previously created resource.

json
{
  "type": "https://api.bank.com/errors/idempotency-conflict",
  "title": "Idempotency Conflict",
  "status": 409,
  "detail": "Idempotency key 'pay-001' already used with different payload.",
  "ErrorCode": "IDEMPOTENCY_CONFLICT",
  "ErrorDescription": "The provided idempotency key matches a previous request with a different payload.",
  "PreviousResourceUri": "/payments/pay-123"
}

PART 3: RATE LIMITING (429) AND RETRY-AFTER

3.1 The 429 Response

When the TPP exceeds 25 RPS (UK/ CDR baseline), the ASPSP returns 429 Too Many Requests with a Retry-After header.

Response:

text
HTTP/1.1 429 Too Many Requests
Retry-After: 3
Content-Type: application/json

{
  "type": "https://api.bank.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded the 25 RPS limit. Please retry after 3 seconds.",
  "ErrorCode": "RATE_LIMIT_EXCEEDED",
  "ErrorDescription": "API rate limit of 25 requests per second has been exceeded."
}

3.2 Token Bucket Algorithm Implementation

python
import time
import threading
from collections import defaultdict

class TokenBucket:
    def __init__(self, rate=25, capacity=50):
        self.rate = rate  # 25 tokens/second
        self.capacity = capacity  # Burst up to 50
        self.tokens = defaultdict(lambda: capacity)
        self.last_refill = defaultdict(lambda: time.time())
        self.lock = threading.Lock()

    def allow_request(self, client_id):
        with self.lock:
            now = time.time()
            # Refill tokens based on time elapsed
            elapsed = now - self.last_refill[client_id]
            new_tokens = elapsed * self.rate
            self.tokens[client_id] = min(self.capacity, self.tokens[client_id] + new_tokens)
            self.last_refill[client_id] = now

            if self.tokens[client_id] >= 1:
                self.tokens[client_id] -= 1
                return True, None
            else:
                # Calculate retry-after
                deficit = 1 - self.tokens[client_id]
                wait_time = deficit / self.rate
                return False, int(wait_time) + 1

Latency overhead: The token bucket check is purely in‑memory (no network calls). It adds ≤0.1ms to the request.


PART 4: CIRCUIT BREAKERS AND 503 — Handling Backend Failures

4.1 The Circuit Breaker Pattern

If the ASPSP’s ledger system or database is failing, returning 500 Internal Server Error for every request is not helpful—it overwhelms the TPP’s retry logic and causes cascading failures. Instead, implement a circuit breaker that returns 503 Service Unavailable when the failure rate exceeds a threshold.

States:

  • CLOSED: Requests pass through. Failure rate monitored.

  • OPEN: Requests are blocked; immediately return 503. (Default: 10 seconds)

  • HALF‑OPEN: Allow limited requests to test if the backend has recovered.

4.2 Python Implementation (Resilience4j‑style)

python
import time
import threading
from enum import Enum

class CircuitBreakerState(Enum):
    CLOSED = 1
    OPEN = 2
    HALF_OPEN = 3

class CircuitBreaker:
    def __init__(self, failure_threshold=0.5, sample_size=10, open_timeout=10):
        self.failure_threshold = failure_threshold
        self.sample_size = sample_size
        self.open_timeout = open_timeout  # seconds
        self.state = CircuitBreakerState.CLOSED
        self.failure_count = 0
        self.total_count = 0
        self.last_open_time = 0
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            if self.state == CircuitBreakerState.OPEN:
                # Check if we should transition to HALF_OPEN
                if time.time() - self.last_open_time > self.open_timeout:
                    self.state = CircuitBreakerState.HALF_OPEN
                    return True
                return False
            return True

    def record_result(self, success):
        with self.lock:
            if self.state == CircuitBreakerState.OPEN:
                return

            self.total_count += 1
            if not success:
                self.failure_count += 1

            # Only evaluate if we have enough samples
            if self.total_count >= self.sample_size:
                failure_rate = self.failure_count / self.total_count
                if failure_rate >= self.failure_threshold:
                    self.state = CircuitBreakerState.OPEN
                    self.last_open_time = time.time()
                    self.failure_count = 0
                    self.total_count = 0

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            if not self.allow_request():
                return 503, {"ErrorCode": "CIRCUIT_OPEN", "detail": "Service temporarily unavailable."}
            try:
                result = func(*args, **kwargs)
                self.record_result(True)
                return result
            except Exception:
                self.record_result(False)
                raise
        return wrapper

4.3 Latency Overhead of Error Handling

 
 
Operation Latency (p95)
Circuit breaker state check (in‑memory) ≤0.05ms
Error JSON serialisation ≤2ms
Audit log write (async) ≤5ms (asynchronous)
Total error overhead ≤7ms

This is negligible compared to the overall latency budget.


CLOSING — OPERATIONAL RISK OF POOR ERROR HANDLING

If error responses are inconsistent, unstandardised, or missing:

  • TPPs cannot programmatically handle errors → they retry indefinitely → thundering herd → system collapse.

  • Regulators cannot audit compliance → missing ErrorCode fields → the bank fails the OBIE compliance test.

  • PSUs receive confusing messages → frustration → complaints to the FCA/Ombudsman.

Key takeaways:

  • RFC 7807 Problem Details is the standard format (adopted by OBIE and CDR).

  • HTTP status codes must be precise (401 vs. 403 vs. 409 vs. 429).

  • Retry-After is mandatory for 429 responses.

  • Circuit breakers prevent cascading failures (return 503 when backend is