From 49b56c4ef25e3468311a05ad7a8ef489a891176f Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:41:25 +0100 Subject: [PATCH 1/9] Add default timeout support to opal api proxy --- src/constants/default-proxy-config.ts | 1 + src/interfaces/proxy-config.ts | 1 + src/proxy/opal-api-proxy/index.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/constants/default-proxy-config.ts b/src/constants/default-proxy-config.ts index d70b245..f3f5ffb 100644 --- a/src/constants/default-proxy-config.ts +++ b/src/constants/default-proxy-config.ts @@ -4,4 +4,5 @@ export const DEFAULT_PROXY_CONFIG: Readonly = Object.freeze( opalFinesServiceUrl: null, opalUserServiceUrl: null, opalRmServiceUrl: null, + timeoutInMilliseconds: 30000, }); diff --git a/src/interfaces/proxy-config.ts b/src/interfaces/proxy-config.ts index 7088952..b88941b 100644 --- a/src/interfaces/proxy-config.ts +++ b/src/interfaces/proxy-config.ts @@ -2,6 +2,7 @@ class ProxyConfiguration { opalFinesServiceUrl: string | null = null; opalUserServiceUrl: string | null = null; opalRmServiceUrl: string | null = null; + timeoutInMilliseconds!: number; constructor(initialValues?: Partial) { Object.assign(this, initialValues); diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index fd560d3..4d1f4ec 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -3,15 +3,21 @@ import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middlewar import { Logger } from '@hmcts/nodejs-logging'; const logger = Logger.getLogger('opalApiProxy'); +import { DEFAULT_PROXY_CONFIG } from '../../constants/default-proxy-config.js'; import { rawJson, verifyContentDigest } from '../middlewares/digest-verify.middleware.js'; import { verifyResponseDigest } from '../utils/response-digest.js'; /** * Creates an Express router that validates request digests and proxies requests to the Opal API. * @param opalApiTarget Upstream Opal API base URL. + * @param timeoutInMilliseconds Maximum time to wait for the upstream proxy request before timing out. * @returns Configured Express router ready to mount in the host app. */ -const opalApiProxy = (opalApiTarget: string, logEnabled: boolean) => { +const opalApiProxy = ( + opalApiTarget: string, + logEnabled: boolean, + timeoutInMilliseconds = DEFAULT_PROXY_CONFIG.timeoutInMilliseconds, +) => { const router = Router(); router.use(rawJson()); @@ -24,6 +30,7 @@ const opalApiProxy = (opalApiTarget: string, logEnabled: boolean) => { const proxy = createProxyMiddleware({ target: opalApiTarget, changeOrigin: true, + proxyTimeout: timeoutInMilliseconds, selfHandleResponse: true, on: { // eslint-disable-next-line @typescript-eslint/no-explicit-any From 6282783239ee88c2faa07fdac3fa275103579b1a Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:22:31 +0100 Subject: [PATCH 2/9] Add proxy timeout error mapping --- src/proxy/opal-api-proxy/index.ts | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index 4d1f4ec..405c653 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -1,12 +1,56 @@ import { Router } from 'express'; import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; import { Logger } from '@hmcts/nodejs-logging'; +import { Socket } from 'node:net'; +import type { ServerResponse } from 'node:http'; const logger = Logger.getLogger('opalApiProxy'); import { DEFAULT_PROXY_CONFIG } from '../../constants/default-proxy-config.js'; import { rawJson, verifyContentDigest } from '../middlewares/digest-verify.middleware.js'; import { verifyResponseDigest } from '../utils/response-digest.js'; +type ProxyErrorResponse = { + title: string; + status: number; + detail: string; + retriable: boolean; +}; + +const RETRYABLE_PROXY_ERROR_CODES = new Set(['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT']); + +function isServerResponse(res: ServerResponse | Socket): res is ServerResponse { + return !(res instanceof Socket); +} + +function isRetryableProxyError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + const code = (error as Error & { code?: unknown }).code; + return typeof code === 'string' && RETRYABLE_PROXY_ERROR_CODES.has(code); +} + +function createProxyErrorResponse( + status: number, + title: string, + detail: string, + retriable: boolean, +): ProxyErrorResponse { + return { title, status, detail, retriable }; +} + +function sendProxyErrorResponse(res: ServerResponse | Socket, body: ProxyErrorResponse): void { + if (!isServerResponse(res) || res.headersSent || res.writableEnded) { + return; + } + + res.statusCode = body.status; + res.setHeader('Content-Type', 'application/problem+json; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + res.end(JSON.stringify(body)); +} + /** * Creates an Express router that validates request digests and proxies requests to the Opal API. * @param opalApiTarget Upstream Opal API base URL. @@ -59,6 +103,30 @@ const opalApiProxy = ( proxyRes: (proxyRes, req, res) => { void handleProxyResponse(proxyRes, req, res); }, + error: (error, _req, res) => { + // Do not replay the request here: only the frontend knows whether it is safe to retry. + if (isRetryableProxyError(error)) { + logger.warn('Proxy timeout or transport failure when calling upstream service', { + message: error.message, + code: (error as Error & { code?: string }).code, + }); + sendProxyErrorResponse( + res, + createProxyErrorResponse(504, 'Gateway Timeout', 'The upstream service did not respond in time.', true), + ); + return; + } + + // Keep other proxy failures deterministic without telling the frontend to retry them. + logger.error('Unexpected proxy failure when calling upstream service', { + message: error instanceof Error ? error.message : String(error), + code: error instanceof Error ? (error as Error & { code?: string }).code : undefined, + }); + sendProxyErrorResponse( + res, + createProxyErrorResponse(502, 'Bad Gateway', 'The upstream service could not be reached.', false), + ); + }, }, }); From c5bcf64354026c9907dc098192a04ae588f4ab3b Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:14:53 +0100 Subject: [PATCH 3/9] Document proxy timeout and retry contract --- src/proxy/opal-api-proxy/README.md | 70 ++++++++++++++++++++++++++++++ src/proxy/opal-api-proxy/index.ts | 8 ++-- 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/proxy/opal-api-proxy/README.md diff --git a/src/proxy/opal-api-proxy/README.md b/src/proxy/opal-api-proxy/README.md new file mode 100644 index 0000000..9460779 --- /dev/null +++ b/src/proxy/opal-api-proxy/README.md @@ -0,0 +1,70 @@ +# Opal API Proxy Timeout and Retry Contract + +`OpalApiProxy` forwards requests from an opal-frontend node server to the opal-fines-service. +It applies a timeout to the upstream connection, but it never retries a request. +Only the consuming opal-frontend can decide whether replaying its original request is +safe. + +## Timeout configuration + +Pass the timeout, in milliseconds, as the third argument when creating the +proxy. If it is omitted, the proxy uses +`DEFAULT_PROXY_CONFIG.timeoutInMilliseconds`, which is `30000` milliseconds. + +```ts +opalApiProxy( + process.env.OPAL_FINES_SERVICE_URL, + false, + 30000, +); +``` + +## Error responses + +When the opal-fines-service times out or the proxy encounters a recognised +transport failure, the proxy returns the following response: + +```json +{ + "title": "Gateway Timeout", + "status": 504, + "detail": "The opal-fines-service did not respond in time.", + "retriable": true +} +``` + +Other unexpected proxy failures return `502 Bad Gateway` with +`"retriable": false`. + +`retriable` is information for the frontend's error and retry handling. It is +not an instruction to replay every request, and the proxy does not replay any +request automatically. + +## Frontend retry example + +The optional retry support in `opal-frontend-common-ui-lib` must be registered +by the consuming application. A request then opts in individually. This is +appropriate only after the application has decided that replaying the request +is safe, for example a read-only `GET` request. + +```ts +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { httpRetryInterceptor } from '@hmcts/opal-frontend-common/interceptors/http-retry'; + +provideHttpClient(withInterceptors([httpRetryInterceptor])); +``` + +```ts +import { withHttpRetry } from '@hmcts/opal-frontend-common/interceptors/http-retry'; + +this.http.get('/api/accounts/123', { + context: withHttpRetry({ + retryCount: 2, + retryableStatusCodes: [504], + }), +}); +``` + +Do not opt mutation requests, such as `POST`, `PUT`, `PATCH`, or `DELETE`, into +automatic retries unless the consuming application has explicitly established +that replaying them is safe. diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index 405c653..b1bf3e4 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -106,25 +106,25 @@ const opalApiProxy = ( error: (error, _req, res) => { // Do not replay the request here: only the frontend knows whether it is safe to retry. if (isRetryableProxyError(error)) { - logger.warn('Proxy timeout or transport failure when calling upstream service', { + logger.warn('Proxy timeout or transport failure when calling opal-fines-service', { message: error.message, code: (error as Error & { code?: string }).code, }); sendProxyErrorResponse( res, - createProxyErrorResponse(504, 'Gateway Timeout', 'The upstream service did not respond in time.', true), + createProxyErrorResponse(504, 'Gateway Timeout', 'The opal-fines-service did not respond in time.', true), ); return; } // Keep other proxy failures deterministic without telling the frontend to retry them. - logger.error('Unexpected proxy failure when calling upstream service', { + logger.error('Unexpected proxy failure when calling opal-fines-service', { message: error instanceof Error ? error.message : String(error), code: error instanceof Error ? (error as Error & { code?: string }).code : undefined, }); sendProxyErrorResponse( res, - createProxyErrorResponse(502, 'Bad Gateway', 'The upstream service could not be reached.', false), + createProxyErrorResponse(502, 'Bad Gateway', 'The opal-fines-service could not be reached.', false), ); }, }, From 1d7b492dba3abc684eb678c58970f1beb5f33ce6 Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:35:36 +0100 Subject: [PATCH 4/9] Require proxy timeout configuration --- src/proxy/opal-api-proxy/README.md | 10 +++++----- src/proxy/opal-api-proxy/index.ts | 31 +++++++++++++++++++----------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/proxy/opal-api-proxy/README.md b/src/proxy/opal-api-proxy/README.md index 9460779..1ffe2fd 100644 --- a/src/proxy/opal-api-proxy/README.md +++ b/src/proxy/opal-api-proxy/README.md @@ -1,6 +1,6 @@ # Opal API Proxy Timeout and Retry Contract -`OpalApiProxy` forwards requests from an opal-frontend node server to the opal-fines-service. +`OpalApiProxy` forwards requests from an opal-frontend node server to an upstream API service. It applies a timeout to the upstream connection, but it never retries a request. Only the consuming opal-frontend can decide whether replaying its original request is safe. @@ -8,8 +8,8 @@ safe. ## Timeout configuration Pass the timeout, in milliseconds, as the third argument when creating the -proxy. If it is omitted, the proxy uses -`DEFAULT_PROXY_CONFIG.timeoutInMilliseconds`, which is `30000` milliseconds. +proxy. The consuming application owns this value so it can be configured per +environment. ```ts opalApiProxy( @@ -21,14 +21,14 @@ opalApiProxy( ## Error responses -When the opal-fines-service times out or the proxy encounters a recognised +When the upstream service times out or the proxy encounters a recognised transport failure, the proxy returns the following response: ```json { "title": "Gateway Timeout", "status": 504, - "detail": "The opal-fines-service did not respond in time.", + "detail": "The upstream service did not respond in time.", "retriable": true } ``` diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index b1bf3e4..67dc689 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -5,7 +5,6 @@ import { Socket } from 'node:net'; import type { ServerResponse } from 'node:http'; const logger = Logger.getLogger('opalApiProxy'); -import { DEFAULT_PROXY_CONFIG } from '../../constants/default-proxy-config.js'; import { rawJson, verifyContentDigest } from '../middlewares/digest-verify.middleware.js'; import { verifyResponseDigest } from '../utils/response-digest.js'; @@ -18,10 +17,16 @@ type ProxyErrorResponse = { const RETRYABLE_PROXY_ERROR_CODES = new Set(['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT']); +/** + * Narrows the proxy error response target to an HTTP response before writing to it. + */ function isServerResponse(res: ServerResponse | Socket): res is ServerResponse { return !(res instanceof Socket); } +/** + * Identifies timeout and transport failures that callers may safely mark as retryable. + */ function isRetryableProxyError(error: unknown): boolean { if (!(error instanceof Error)) { return false; @@ -31,6 +36,9 @@ function isRetryableProxyError(error: unknown): boolean { return typeof code === 'string' && RETRYABLE_PROXY_ERROR_CODES.has(code); } +/** + * Creates the problem response body returned by the proxy error handler. + */ function createProxyErrorResponse( status: number, title: string, @@ -40,6 +48,9 @@ function createProxyErrorResponse( return { title, status, detail, retriable }; } +/** + * Writes a deterministic proxy error response when Express has not already sent one. + */ function sendProxyErrorResponse(res: ServerResponse | Socket, body: ProxyErrorResponse): void { if (!isServerResponse(res) || res.headersSent || res.writableEnded) { return; @@ -54,14 +65,12 @@ function sendProxyErrorResponse(res: ServerResponse | Socket, body: ProxyErrorRe /** * Creates an Express router that validates request digests and proxies requests to the Opal API. * @param opalApiTarget Upstream Opal API base URL. + * @param logEnabled Whether to log the client IP address added to the upstream request. * @param timeoutInMilliseconds Maximum time to wait for the upstream proxy request before timing out. - * @returns Configured Express router ready to mount in the host app. + * This is intentionally required so the consuming app owns environment-specific timeout configuration. + * @returns Configured Express router that maps proxy timeout and transport failures without replaying requests. */ -const opalApiProxy = ( - opalApiTarget: string, - logEnabled: boolean, - timeoutInMilliseconds = DEFAULT_PROXY_CONFIG.timeoutInMilliseconds, -) => { +const opalApiProxy = (opalApiTarget: string, logEnabled: boolean, timeoutInMilliseconds: number) => { const router = Router(); router.use(rawJson()); @@ -106,25 +115,25 @@ const opalApiProxy = ( error: (error, _req, res) => { // Do not replay the request here: only the frontend knows whether it is safe to retry. if (isRetryableProxyError(error)) { - logger.warn('Proxy timeout or transport failure when calling opal-fines-service', { + logger.warn('Proxy timeout or transport failure when calling upstream service', { message: error.message, code: (error as Error & { code?: string }).code, }); sendProxyErrorResponse( res, - createProxyErrorResponse(504, 'Gateway Timeout', 'The opal-fines-service did not respond in time.', true), + createProxyErrorResponse(504, 'Gateway Timeout', 'The upstream service did not respond in time.', true), ); return; } // Keep other proxy failures deterministic without telling the frontend to retry them. - logger.error('Unexpected proxy failure when calling opal-fines-service', { + logger.error('Unexpected proxy failure when calling upstream service', { message: error instanceof Error ? error.message : String(error), code: error instanceof Error ? (error as Error & { code?: string }).code : undefined, }); sendProxyErrorResponse( res, - createProxyErrorResponse(502, 'Bad Gateway', 'The opal-fines-service could not be reached.', false), + createProxyErrorResponse(502, 'Bad Gateway', 'The upstream service could not be reached.', false), ); }, }, From d2d9574db1ac87aa72b301196e09c7c4856d24b5 Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:19:19 +0100 Subject: [PATCH 5/9] Update src/constants/default-proxy-config.ts Co-authored-by: Frankie Moran --- src/constants/default-proxy-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/default-proxy-config.ts b/src/constants/default-proxy-config.ts index f3f5ffb..f6fe662 100644 --- a/src/constants/default-proxy-config.ts +++ b/src/constants/default-proxy-config.ts @@ -4,5 +4,5 @@ export const DEFAULT_PROXY_CONFIG: Readonly = Object.freeze( opalFinesServiceUrl: null, opalUserServiceUrl: null, opalRmServiceUrl: null, - timeoutInMilliseconds: 30000, + timeoutInMilliseconds: null, }); From 608953c0ba95590d4bed2727d665a621a343db2a Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:44:55 +0100 Subject: [PATCH 6/9] Clarify proxy timeout ownership --- README.md | 31 +++++++++++++++++++++++++++++++ package.json | 2 +- src/interfaces/proxy-config.ts | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 20e19c2..87e6ce0 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,37 @@ import { Refer to the `exports` block in `package.json` for the full list of available modules. +## API Proxy Timeout Configuration + +`DEFAULT_PROXY_CONFIG.timeoutInMilliseconds` is intentionally `null`. The common node library does not own an +environment-specific proxy timeout value. + +Consuming applications must provide a positive timeout value, in milliseconds, before wiring `OpalApiProxy`. +The proxy factory requires a `number` so a consumer cannot intentionally create the proxy with a missing timeout. + +```ts +const proxyConfiguration: ProxyConfiguration = { + ...DEFAULT_PROXY_CONFIG, + opalFinesServiceUrl: config.get('opal-api.opal-fines-service'), + opalUserServiceUrl: config.get('opal-api.opal-user-service'), + timeoutInMilliseconds: config.get('opal-api.timeoutInMilliseconds'), +}; + +if (proxyConfiguration.timeoutInMilliseconds === null) { + throw new Error('Missing opal-api.timeoutInMilliseconds configuration.'); +} + +if (proxyConfiguration.opalFinesServiceUrl) { + app.use( + '/opal-fines-service', + OpalApiProxy(proxyConfiguration.opalFinesServiceUrl, ipLoggingEnabled, proxyConfiguration.timeoutInMilliseconds), + ); +} +``` + +The proxy maps timeout and recognised transport failures to deterministic error responses, but it never retries +requests. Frontend retry behaviour remains opt-in at the request level. + ## Commonly Used Commands The following commands are available in the `package.json`: diff --git a/package.json b/package.json index 2ac3fe4..4e81f8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hmcts/opal-frontend-common-node", "type": "module", - "version": "0.0.42", + "version": "0.0.43", "license": "MIT", "description": "Common nodejs library components for opal", "engines": { diff --git a/src/interfaces/proxy-config.ts b/src/interfaces/proxy-config.ts index b88941b..3456499 100644 --- a/src/interfaces/proxy-config.ts +++ b/src/interfaces/proxy-config.ts @@ -2,7 +2,7 @@ class ProxyConfiguration { opalFinesServiceUrl: string | null = null; opalUserServiceUrl: string | null = null; opalRmServiceUrl: string | null = null; - timeoutInMilliseconds!: number; + timeoutInMilliseconds: number | null = null; constructor(initialValues?: Partial) { Object.assign(this, initialValues); From ffaff6ae515f4dc493eb256944978605a6644645 Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:08:18 +0100 Subject: [PATCH 7/9] Move proxy timeout docs to proxy README --- README.md | 31 ------------------------------ src/proxy/opal-api-proxy/README.md | 25 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 87e6ce0..20e19c2 100644 --- a/README.md +++ b/README.md @@ -175,37 +175,6 @@ import { Refer to the `exports` block in `package.json` for the full list of available modules. -## API Proxy Timeout Configuration - -`DEFAULT_PROXY_CONFIG.timeoutInMilliseconds` is intentionally `null`. The common node library does not own an -environment-specific proxy timeout value. - -Consuming applications must provide a positive timeout value, in milliseconds, before wiring `OpalApiProxy`. -The proxy factory requires a `number` so a consumer cannot intentionally create the proxy with a missing timeout. - -```ts -const proxyConfiguration: ProxyConfiguration = { - ...DEFAULT_PROXY_CONFIG, - opalFinesServiceUrl: config.get('opal-api.opal-fines-service'), - opalUserServiceUrl: config.get('opal-api.opal-user-service'), - timeoutInMilliseconds: config.get('opal-api.timeoutInMilliseconds'), -}; - -if (proxyConfiguration.timeoutInMilliseconds === null) { - throw new Error('Missing opal-api.timeoutInMilliseconds configuration.'); -} - -if (proxyConfiguration.opalFinesServiceUrl) { - app.use( - '/opal-fines-service', - OpalApiProxy(proxyConfiguration.opalFinesServiceUrl, ipLoggingEnabled, proxyConfiguration.timeoutInMilliseconds), - ); -} -``` - -The proxy maps timeout and recognised transport failures to deterministic error responses, but it never retries -requests. Frontend retry behaviour remains opt-in at the request level. - ## Commonly Used Commands The following commands are available in the `package.json`: diff --git a/src/proxy/opal-api-proxy/README.md b/src/proxy/opal-api-proxy/README.md index 1ffe2fd..47ce578 100644 --- a/src/proxy/opal-api-proxy/README.md +++ b/src/proxy/opal-api-proxy/README.md @@ -7,16 +7,31 @@ safe. ## Timeout configuration +`DEFAULT_PROXY_CONFIG.timeoutInMilliseconds` is intentionally `null`. The common node library does not own an +environment-specific proxy timeout value. + Pass the timeout, in milliseconds, as the third argument when creating the proxy. The consuming application owns this value so it can be configured per environment. ```ts -opalApiProxy( - process.env.OPAL_FINES_SERVICE_URL, - false, - 30000, -); +const proxyConfiguration: ProxyConfiguration = { + ...DEFAULT_PROXY_CONFIG, + opalFinesServiceUrl: config.get('opal-api.opal-fines-service'), + opalUserServiceUrl: config.get('opal-api.opal-user-service'), + timeoutInMilliseconds: config.get('opal-api.timeoutInMilliseconds'), +}; + +if (proxyConfiguration.timeoutInMilliseconds === null) { + throw new Error('Missing opal-api.timeoutInMilliseconds configuration.'); +} + +if (proxyConfiguration.opalFinesServiceUrl) { + app.use( + '/opal-fines-service', + OpalApiProxy(proxyConfiguration.opalFinesServiceUrl, ipLoggingEnabled, proxyConfiguration.timeoutInMilliseconds), + ); +} ``` ## Error responses From 1f2561727a03ea67c78ebd9c472aa9dad6d6591c Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:28:43 +0100 Subject: [PATCH 8/9] Log proxy target on failures --- src/proxy/opal-api-proxy/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index 67dc689..5626a66 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -115,7 +115,8 @@ const opalApiProxy = (opalApiTarget: string, logEnabled: boolean, timeoutInMilli error: (error, _req, res) => { // Do not replay the request here: only the frontend knows whether it is safe to retry. if (isRetryableProxyError(error)) { - logger.warn('Proxy timeout or transport failure when calling upstream service', { + logger.warn(`Proxy timeout or transport failure when calling ${opalApiTarget}`, { + target: opalApiTarget, message: error.message, code: (error as Error & { code?: string }).code, }); @@ -127,7 +128,8 @@ const opalApiProxy = (opalApiTarget: string, logEnabled: boolean, timeoutInMilli } // Keep other proxy failures deterministic without telling the frontend to retry them. - logger.error('Unexpected proxy failure when calling upstream service', { + logger.error(`Unexpected proxy failure when calling ${opalApiTarget}`, { + target: opalApiTarget, message: error instanceof Error ? error.message : String(error), code: error instanceof Error ? (error as Error & { code?: string }).code : undefined, }); From 790aac587eeafa442871ee3e3b48840ca43c4b19 Mon Sep 17 00:00:00 2001 From: TimDanielsCQI <275969854+TimDanielsCQI@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:54:00 +0100 Subject: [PATCH 9/9] Clarify backend proxy wording --- src/proxy/opal-api-proxy/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/proxy/opal-api-proxy/index.ts b/src/proxy/opal-api-proxy/index.ts index 5626a66..5739653 100644 --- a/src/proxy/opal-api-proxy/index.ts +++ b/src/proxy/opal-api-proxy/index.ts @@ -64,9 +64,9 @@ function sendProxyErrorResponse(res: ServerResponse | Socket, body: ProxyErrorRe /** * Creates an Express router that validates request digests and proxies requests to the Opal API. - * @param opalApiTarget Upstream Opal API base URL. - * @param logEnabled Whether to log the client IP address added to the upstream request. - * @param timeoutInMilliseconds Maximum time to wait for the upstream proxy request before timing out. + * @param opalApiTarget Backend Opal API base URL. + * @param logEnabled Whether to log the client IP address added to the backend request. + * @param timeoutInMilliseconds Maximum time to wait for the backend proxy request before timing out. * This is intentionally required so the consuming app owns environment-specific timeout configuration. * @returns Configured Express router that maps proxy timeout and transport failures without replaying requests. */ @@ -122,7 +122,7 @@ const opalApiProxy = (opalApiTarget: string, logEnabled: boolean, timeoutInMilli }); sendProxyErrorResponse( res, - createProxyErrorResponse(504, 'Gateway Timeout', 'The upstream service did not respond in time.', true), + createProxyErrorResponse(504, 'Gateway Timeout', 'The backend service did not respond in time.', true), ); return; } @@ -135,7 +135,7 @@ const opalApiProxy = (opalApiTarget: string, logEnabled: boolean, timeoutInMilli }); sendProxyErrorResponse( res, - createProxyErrorResponse(502, 'Bad Gateway', 'The upstream service could not be reached.', false), + createProxyErrorResponse(502, 'Bad Gateway', 'The backend service could not be reached.', false), ); }, },