| applyTo | proxies/** |
|---|---|
| description | Apigee proxy architecture, request flow, policies, shared flows, and how to modify proxy configuration |
The proxies/ directory contains two independent Apigee API proxy bundles that share the same base path ({{ SERVICE_BASE_PATH }} → /referrals).
Forwards requests to the real e-RS backend. This is the most policy-heavy part of the repo.
| Path | Purpose |
|---|---|
apiproxy/ers.xml |
Root proxy descriptor |
apiproxy/proxies/default.xml |
ProxyEndpoint — inbound routing (_ping, _status, catch-all to target) |
apiproxy/targets/ers-target.xml |
TargetEndpoint — outbound to backend via {{ ERS_TARGET_SERVER }}, plus all auth/error handling |
apiproxy/policies/ |
~60 XML policy files (see breakdown below) |
apiproxy/resources/jsc/ |
Inline JavaScript used by policies (IsFhirR4Path.js, SetCurrentTimestamp.js, SetStatusResponse.js) |
javascript.IsFhirR4Path— setsisFhirR4Pathboolean by matching/FHIR/R4/in the path suffix; this flag drives R4-vs-STU3 branching throughoutOauthV2.VerifyAccessToken— validates OAuth2 token; accepted scopes:app:level3,user-nhs-id:aal3,user-nhs-id:aal2FlowCallout.ExtendedAttributes+FlowCallout.EUOAllowlistVerify— (user-restricted only, excluding/FHIR/R4/PractitionerRole) validates the end-user organisation ODS code against an allowlist- ASID validation —
RaiseFault.MissingAsidifapp.asidis empty; thenAssignMessage.PopulateAsidFromApp+AssignMessage.SetAsidHeadercopy the ASID onto the request AssignMessage.AddBaseUrlHeader— adds the base URL header for the backendFlowCallout.ApplyRateLimiting— spike arrest + quota enforcement (delegates to external shared flow — rate/quota values are NOT in this repo)
Several FlowCallout.* policies delegate to shared flows that are NOT defined in this repo:
ApplyRateLimiting— spike arrest and quota enforcement (rate/quota values configured externally)ExtendedAttributes— retrieves extended app attributesEUOAllowlistVerify— validates ODS code against the end-user organisation allowlistLogToSplunk— audit logging
These shared flows are deployed separately to the Apigee environment. This repo only controls when they are called and how their errors are handled.
user-restricted-flow(accesstoken.auth_type == "user") — rejects app-only business functions (AUTHORISED_APPLICATION), swaps NHSD headers from external to internal naming, sets AAL/IAL/AMR headers, and enforces IAL ≥ 3app-restricted-flow(accesstoken.auth_type == "app") — rejects any manually-setx-ers-*headers (403), then sets fixed app-restricted values for ODS, business function, user-id, and access-modeundefined-flow— catch-all that returns 403 (should never trigger)
Both flows finish with AssignMessage.Swap.CorrelationHeader which converts X-Correlation-ID → NHSD-Correlation-ID for the backend.
The proxy swaps external consumer-facing headers to internal backend headers:
| External header (consumer sends) | Internal header (backend receives) |
|---|---|
X-Correlation-ID |
NHSD-Correlation-ID (appended with .{messageid}) |
nhsd-end-user-organisation-ods |
x-ers-ods-code |
nhsd-ers-business-function |
x-ers-business-function |
nhsd-ers-comm-rule-org |
x-ers-comm-rule-org |
nhsd-ers-file-name |
x-ers-file-name |
nhsd-ers-referral-id |
x-ers-referral-id |
NHSD-eRS-On-Behalf-Of-User-ID |
x-ers-on-behalf-of-user-id |
Additional headers set by the proxy (not from consumer input):
x-ers-access-mode—userorappx-ers-user-id— from OAuth token (user) or app config (app)x-ers-authentication-assurance-level— from tokenx-ers-id-assurance-level— from tokenx-ers-amr— authentication method reference from token
- Sets
X-Request-IDflag, swapsx_ers_transaction_idto the response, removesnhsd-correlation-idfrom the response
Error responses are FHIR-version-aware — isFhirR4Path selects between R4 and pre-R4 OperationOutcome response shapes. Handled faults:
- OAuth token failures (scope errors → AAL insufficient)
- Spike arrest / quota exceeded (rate limiting)
- Insufficient IAL (identity assurance level < 3)
- Missing ASID
- Invalid business function
- ODS header missing / not in partner allowlist
- EUO allowlist internal errors (500)
- Target server:
{{ ERS_TARGET_SERVER }}(defaults toe-referrals-service-api) - Backend path:
/ers-api - TLS enabled; conditional truststore for feature-test (
--ft-) environments - I/O timeout: 180 seconds
- Jinja2 templating (
{{ }}placeholders) is resolved at build time byscripts/build_proxy.sh
AssignMessage.Set.*— set a header/variable to a fixed valueAssignMessage.Swap.*— rename/transform a header between external and internal namingAssignMessage.Remove.*— strip a headerAssignMessage.SetOperationOutcome*— prepare FHIR OperationOutcome error variablesRaiseFault.*— return an HTTP error statusFlowCallout.*— delegate to a shared flowKeyValueMapOperations.*— read from Apigee KVM stores
Lightweight proxy that forwards to the sandbox container (Hapi.js mock server) via Apigee hosted targets.
- Only ~11 policies (vs ~60 in live) — no OAuth, no ASID, no rate limiting, no header swapping
- Adds CORS preflight handling (
AssignMessage.AddCorson OPTIONS) - Uses
DecodeJWT.FromJWTHeaderto decode (but not validate) the JWT for inspection - Target is
{{ HOSTED_TARGET_CONNECTION }}— the sandbox container deployed alongside the proxy - No fault rules or auth enforcement
Both proxies share _ping and _status flows — _ping returns a canned response directly (no backend call), _status uses an API key from KVM + ServiceCallout.CallHealthcheckEndpoint.
scripts/build_proxy.sh copies both proxy bundles into build/proxies/, and for sandbox, rsyncs the entire sandbox/ app into build/proxies/sandbox/apiproxy/resources/hosted/ so Apigee can run it as a hosted target.
Jinja2 template variables (e.g. {{ SERVICE_BASE_PATH }}, {{ ERS_TARGET_SERVER }}, {{ HOSTED_TARGET_CONNECTION }}) are resolved during the CI/CD pipeline deployment step.
- Apigee API gateway managed via Terraform (
terraform/main.tf) - Uses the
api-platform-service-modulefrom NHSDigital - Backend: Azure (
azurermstate backend) - Proxy type auto-selects
sandboxorlivebased on Apigee environment name
- Create/edit the XML file in
proxies/live/apiproxy/policies/(orsandbox/) - Reference it by
<Name>in the appropriate flow inproxies/default.xmlorers-target.xml - If it uses JavaScript, add the
.jsfile toapiproxy/resources/jsc/and reference via<ResourceURL>jsc://filename.js</ResourceURL> - Run
make lint— validates all proxy XML viascripts/xml_validator.py - Remember to handle both R4 and pre-R4 paths if the policy produces FHIR error responses