---
status: accepted
date: 2026-02-03
decision-makers: Constructor Fabric Steering Committee
---

# Error Source Distinction — Response Header Indicator for Gateway vs Upstream Errors


<!-- toc -->

- [Context and Problem Statement](#context-and-problem-statement)
- [Decision Drivers](#decision-drivers)
- [Considered Options](#considered-options)
- [Decision Outcome](#decision-outcome)
  - [Header Values](#header-values)
  - [Consequences](#consequences)
  - [Confirmation](#confirmation)
- [Pros and Cons of the Options](#pros-and-cons-of-the-options)
  - [Response header indicator](#response-header-indicator)
  - [Wrapped error envelope (opt-in)](#wrapped-error-envelope-opt-in)
  - [Distinct status code ranges](#distinct-status-code-ranges)
- [More Information](#more-information)
- [Appendix A: Detailed Error Response Examples](#appendix-a-detailed-error-response-examples)
  - [Gateway Error — RFC 9457 Problem Details](#gateway-error--rfc-9457-problem-details)
  - [Upstream Error — Passthrough](#upstream-error--passthrough)
  - [X-OAGW-Target-Host Error Examples](#x-oagw-target-host-error-examples)
- [Links](#links)
- [Traceability](#traceability)

<!-- /toc -->

**ID**: `cpt-cf-oagw-adr-error-source-distinction`

## Context and Problem Statement

OAGW acts as a proxy between clients and upstream services. Both OAGW and upstream services can return errors. Clients need a reliable way to distinguish between gateway errors (generated by OAGW, e.g., rate limiting, route not found, authentication failure, timeout) and upstream errors (returned by the upstream service and passed through). Without clear distinction, clients cannot determine the appropriate error handling strategy.

## Decision Drivers

* Client developer experience
* Compatibility with existing HTTP clients and tooling
* Support for non-JSON response bodies
* Minimal overhead for success path
* Works across all supported protocols (HTTP, gRPC, WebSocket, WebTransport, SSE)

## Considered Options

* Response header indicator (`X-OAGW-Error-Source`)
* Wrapped error envelope (opt-in)
* Distinct status code ranges

## Decision Outcome

Chosen option: "Response header indicator — `X-OAGW-Error-Source: gateway|upstream`", because it is simple, non-invasive, works with any content type, and is an industry-standard pattern.

### Header Values

- `X-OAGW-Error-Source: gateway` — Error generated by OAGW (rate limit, auth failure, route not found, timeout, circuit breaker)
- `X-OAGW-Error-Source: upstream` — Error returned by the upstream service, passed through unchanged

### Consequences

* Good, because straightforward to implement and consume
* Good, because works with any content type (JSON, binary, streaming)
* Good, because upstream responses passed through unchanged (no body manipulation)
* Good, because industry standard pattern (Kong: `X-Kong-Upstream-Status`, Apigee: `X-Apigee-fault-source`)
* Good, because works with SSE, WebSockets, gRPC streams
* Bad, because headers can be stripped by intermediaries (rare in practice, acceptable risk)
* Bad, because clients must check header explicitly (simple conditional logic)

### Confirmation

Integration tests verify: gateway errors include `X-OAGW-Error-Source: gateway`, upstream errors include `X-OAGW-Error-Source: upstream`, success responses include the header, header present for all supported protocols.

## Pros and Cons of the Options

### Response header indicator

Add a header to every response indicating the error source:

```http
# OAGW-generated error
HTTP/1.1 429 Too Many Requests
X-OAGW-Error-Source: gateway
Content-Type: application/json

{"code": "RATE_LIMIT_EXCEEDED", "message": "..."}

# Upstream error (passthrough)
HTTP/1.1 429 Too Many Requests
X-OAGW-Error-Source: upstream
Content-Type: application/json

<upstream body as-is>
```

* Good, because simple implementation and consumption
* Good, because non-invasive (no body modification)
* Good, because content-type agnostic
* Good, because works with streaming
* Bad, because headers can be stripped by intermediaries
* Bad, because clients must explicitly check header

**Used by**

- Kong Gateway: `X-Kong-Upstream-Status` header
- Apigee: `X-Apigee-fault-source` header

### Wrapped error envelope (opt-in)

Client opts in via `X-OAGW-Response-Mode: envelope`; OAGW wraps all responses.

```http
# Request
X-OAGW-Response-Mode: envelope

# OAGW error response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "oagw": {
    "source": "gateway",
    "status": 429,
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded"
  }
}

# Upstream response (success or error)
HTTP/1.1 200 OK
Content-Type: application/json

{
  "oagw": {
    "source": "upstream",
    "status": 500
  },
  "body": { ... upstream JSON ... }
}
```

* Good, because unambiguous structured distinction
* Good, because extensible (easy to add metadata)
* Bad, because breaks HTTP status code semantics
* Bad, because problematic for non-JSON/streaming responses
* Bad, because clients must opt-in and unwrap

### Distinct status code ranges

OAGW uses specific status codes for gateway-originated errors:

| Source | Status Codes |
|---|---|
| Gateway | 560-569 (custom range) or strictly 502, 503, 504 |
| Upstream | Passthrough as-is |

* Good, because status code alone identifies source
* Bad, because custom codes violate HTTP spec
* Bad, because collision risk (upstream may return same codes)
* Bad, because limited error detail in status code

## More Information

| Criteria | Header | Envelope | Status Code |
|---|:---:|:---:|:---:|
| Simple implementation | Yes | Partial | Yes |
| Simple consumption | Yes | Partial | Yes |
| Works with non-JSON | Yes | No | Yes |
| No body modification | Yes | No | Yes |
| Standards compliant | Yes | Partial | No |
| Reliable detection | Partial | Yes | Partial |
| Works with streaming | Yes | No | Yes |

Envelope option rejected: breaks HTTP semantics, problematic for non-JSON/streaming, adds unwrap complexity.

Status code option rejected: custom 5xx codes violate HTTP spec, collision risk with upstream errors.

## Appendix A: Detailed Error Response Examples

### Gateway Error — RFC 9457 Problem Details

All gateway errors use `application/problem+json` format with GTS type identifiers:

```http
HTTP/1.1 429 Too Many Requests
X-OAGW-Error-Source: gateway
Content-Type: application/problem+json
Retry-After: 15

{
  "type": "gts.cf.core.errors.err.v1~cf.oagw.rate_limit.exceeded.v1",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "Rate limit exceeded for upstream api.openai.com",
  "instance": "/api/oagw/v1/proxy/api.openai.com/v1/chat/completions",
  "upstream_id": "uuid-123",
  "host": "api.openai.com",
  "retry_after_seconds": 15,
  "trace_id": "01J..."
}
```

**Standard Fields** (RFC 9457):
- `type`: GTS identifier for the error type (used for programmatic error handling)
- `title`: Human-readable summary
- `status`: HTTP status code
- `detail`: Human-readable explanation specific to this occurrence
- `instance`: URI reference identifying the specific occurrence

**Extension Fields** (OAGW-specific):
- `upstream_id`, `host`, `path`: Request context
- `retry_after_seconds`: Retry guidance
- `trace_id`: For distributed tracing correlation

### Upstream Error — Passthrough

Upstream errors are passed through unchanged. The body is the upstream's original response:

```http
HTTP/1.1 500 Internal Server Error
X-OAGW-Error-Source: upstream
Content-Type: application/json

<upstream error body as-is>
```

### X-OAGW-Target-Host Error Examples

**Missing Target Host Header** (multi-endpoint upstream with common suffix):

```http
HTTP/1.1 400 Bad Request
X-OAGW-Error-Source: gateway
Content-Type: application/problem+json

{
  "type": "gts.cf.core.errors.err.v1~cf.oagw.routing.missing_target_host.v1",
  "title": "Missing Target Host Header",
  "status": 400,
  "detail": "X-OAGW-Target-Host header required for multi-endpoint upstream with common suffix alias. Valid hosts: [us.vendor.com, eu.vendor.com]",
  "instance": "/api/oagw/v1/proxy/vendor.com/v1/api/resource",
  "upstream_id": "gts.cf.core.oagw.upstream.v1~7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "alias": "vendor.com",
  "valid_hosts": ["us.vendor.com", "eu.vendor.com"],
  "trace_id": "01J..."
}
```

**Invalid Target Host Format**:

```http
HTTP/1.1 400 Bad Request
X-OAGW-Error-Source: gateway
Content-Type: application/problem+json

{
  "type": "gts.cf.core.errors.err.v1~cf.oagw.routing.invalid_target_host.v1",
  "title": "Invalid Target Host Format",
  "status": 400,
  "detail": "X-OAGW-Target-Host must be a valid hostname or IP address (no port, path, or special characters)",
  "instance": "/api/oagw/v1/proxy/vendor.com/v1/api/resource",
  "upstream_id": "gts.cf.core.oagw.upstream.v1~7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "invalid_value": "us.vendor.com:8443",
  "trace_id": "01J..."
}
```

**Unknown Target Host**:

```http
HTTP/1.1 400 Bad Request
X-OAGW-Error-Source: gateway
Content-Type: application/problem+json

{
  "type": "gts.cf.core.errors.err.v1~cf.oagw.routing.unknown_target_host.v1",
  "title": "Unknown Target Host",
  "status": 400,
  "detail": "X-OAGW-Target-Host 'apac.vendor.com' does not match any configured endpoint. Valid hosts: [us.vendor.com, eu.vendor.com]",
  "instance": "/api/oagw/v1/proxy/vendor.com/v1/api/resource",
  "upstream_id": "gts.cf.core.oagw.upstream.v1~7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "invalid_value": "apac.vendor.com",
  "valid_hosts": ["us.vendor.com", "eu.vendor.com"],
  "trace_id": "01J..."
}
```

## Links

- [Plain HTTP Example](../examples/1.http.positive.md)
- [Kong X-Kong-Upstream-Status header](https://developer.konghq.com/gateway/configuration/#headers)
- [Apigee X-Apigee-fault-source header](https://docs.apigee.com/api-platform/troubleshoot/runtime/503-service-unavailable-backend-server)

## Traceability

- **PRD**: [PRD.md](../PRD.md)
- **DESIGN**: [DESIGN.md](../DESIGN.md)

This decision directly addresses the following requirements or design elements:

* `cpt-cf-oagw-fr-request-proxy` — Error source distinction during proxy response handling
* `cpt-cf-oagw-nfr-observability` — Clear error attribution in logs and responses
* `cpt-cf-oagw-fr-streaming` — Error source header works with streaming protocols
