From 7fc7a8f8f12655c89b7526938ce94980ff52bb78 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 14 Jul 2026 16:13:52 +0100 Subject: [PATCH 1/4] feat: Add headers to understand who responded This change adds two headers to each response: - `X-Gu-Backend-App` which represents which app is running (tag-page-rendering, article-rendering, etc.) - `X-Gu-Backend-App-Target-Group` which represents which target group served the request (EC2 or ECS) One use-case for this is in the "run DCR on ECS" project. This is similar to `guardian/frontend`, see https://github.com/guardian/frontend/blob/0cbc1c8448d6afb74eab75903b16a224fbcf1114/common/app/http/Filters.scala#L49-L62. --- dotcom-rendering/src/server/server.dev.ts | 25 +++++++++++++++++++- dotcom-rendering/src/server/server.prod.ts | 27 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts index a8915867297..1af8873b2d4 100644 --- a/dotcom-rendering/src/server/server.dev.ts +++ b/dotcom-rendering/src/server/server.dev.ts @@ -1,4 +1,11 @@ -import { type ErrorRequestHandler, type Handler, Router } from 'express'; +import { + type ErrorRequestHandler, + type Handler, + type NextFunction, + type Request, + type Response, + Router, +} from 'express'; import { pages } from '../devServer/routers/pages'; import { targets } from '../devServer/routers/targets'; import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web'; @@ -61,6 +68,8 @@ const editionalisefront = (url: string): string => { return url; }; +const backendApp = process.env.GU_APP ?? 'dev'; + const redirects: Handler = (req, res, next) => { const path = req.path.split('/')[1]; @@ -105,6 +114,20 @@ const renderer = Router(); renderer.use(getContentFromURLMiddleware); renderer.use(getABTestsFromQueryParams); renderer.use(requestLoggerMiddleware); + +renderer.use((req: Request, res: Response, next: NextFunction) => { + const headers = { + 'X-Gu-Backend-App': backendApp, + 'X-Gu-Backend-App-Target-Group': 'dev', + }; + + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value); + } + + next(); +}); + renderer.get('/Article/*url', handleArticle); renderer.get('/Interactive/*url', handleInteractive); renderer.get('/Blocks/*url', handleBlocks); diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts index db44443d436..d5016b4a5e5 100644 --- a/dotcom-rendering/src/server/server.prod.ts +++ b/dotcom-rendering/src/server/server.prod.ts @@ -1,5 +1,10 @@ import compression from 'compression'; -import type { ErrorRequestHandler, Request, Response } from 'express'; +import type { + ErrorRequestHandler, + NextFunction, + Request, + Response, +} from 'express'; import express from 'express'; import { NotRenderableInDCR } from '../lib/errors/not-renderable-in-dcr'; import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web'; @@ -32,6 +37,13 @@ import { logger } from './lib/logging'; import { requestLoggerMiddleware } from './lib/logging-middleware'; import { recordError } from './lib/logging-store'; +// TODO extract this, and other places that read env vars, into a `config` object? +// See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-environment-variables.html +const runningInECS = + process.env.AWS_EXECUTION_ENV?.startsWith('AWS_ECS_') === true; + +const backendApp = process.env.GU_APP ?? 'unknown'; + export const prodServer = (): void => { logger.info('dotcom-rendering is GO.'); @@ -41,6 +53,19 @@ export const prodServer = (): void => { app.use(requestLoggerMiddleware); app.use(compression()); + app.use((req: Request, res: Response, next: NextFunction) => { + const headers = { + 'X-Gu-Backend-App': backendApp, + 'X-Gu-Backend-App-Target-Group': runningInECS ? 'ecs' : 'ec2', + }; + + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value); + } + + next(); + }); + app.get('/_healthcheck', (req: Request, res: Response) => { res.status(200).send('OKAY'); }); From 17e24d86bfe3d9bb056968f224c035dbebb2371d Mon Sep 17 00:00:00 2001 From: akash1810 Date: Wed, 15 Jul 2026 14:46:39 +0100 Subject: [PATCH 2/4] refactor: Extract duplicated logic into a middleware function --- .../src/server/lib/header-middleware.ts | 36 +++++++++++++++++++ dotcom-rendering/src/server/server.dev.ts | 26 ++------------ dotcom-rendering/src/server/server.prod.ts | 29 ++------------- 3 files changed, 42 insertions(+), 49 deletions(-) create mode 100644 dotcom-rendering/src/server/lib/header-middleware.ts diff --git a/dotcom-rendering/src/server/lib/header-middleware.ts b/dotcom-rendering/src/server/lib/header-middleware.ts new file mode 100644 index 00000000000..5293eacef62 --- /dev/null +++ b/dotcom-rendering/src/server/lib/header-middleware.ts @@ -0,0 +1,36 @@ +import type { NextFunction, Request, RequestHandler, Response } from 'express'; + +const getTargetGroupHeaderValue = () => { + if (process.env.NODE_ENV === 'production') { + // See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-environment-variables.html + return process.env.AWS_EXECUTION_ENV?.startsWith('AWS_ECS_') === true + ? 'ecs' + : 'ec2'; + } + + return 'dev'; +}; + +const backendApp = process.env.GU_APP ?? 'dev'; +const targetGroup = getTargetGroupHeaderValue(); + +/** + * Middleware to add response headers useful for debugging. + * + * @see https://expressjs.com/en/guide/using-middleware + */ +export const responseHeaderMiddleware: RequestHandler = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const headers = { + 'X-Gu-Backend-App': backendApp, + 'X-Gu-Backend-App-Target-Group': targetGroup, + }; + + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value); + } + next(); +}; diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts index 1af8873b2d4..0e7e9f5f440 100644 --- a/dotcom-rendering/src/server/server.dev.ts +++ b/dotcom-rendering/src/server/server.dev.ts @@ -1,11 +1,4 @@ -import { - type ErrorRequestHandler, - type Handler, - type NextFunction, - type Request, - type Response, - Router, -} from 'express'; +import { type ErrorRequestHandler, type Handler, Router } from 'express'; import { pages } from '../devServer/routers/pages'; import { targets } from '../devServer/routers/targets'; import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web'; @@ -35,6 +28,7 @@ import { import { handleAppsThrasher } from './handler.thrasher.apps'; import { getABTestsFromQueryParams } from './lib/get-abtests-from-query-params'; import { getContentFromURLMiddleware } from './lib/get-content-from-url'; +import { responseHeaderMiddleware } from './lib/header-middleware'; import { requestLoggerMiddleware } from './lib/logging-middleware'; import { recordError } from './lib/logging-store'; @@ -68,8 +62,6 @@ const editionalisefront = (url: string): string => { return url; }; -const backendApp = process.env.GU_APP ?? 'dev'; - const redirects: Handler = (req, res, next) => { const path = req.path.split('/')[1]; @@ -114,19 +106,7 @@ const renderer = Router(); renderer.use(getContentFromURLMiddleware); renderer.use(getABTestsFromQueryParams); renderer.use(requestLoggerMiddleware); - -renderer.use((req: Request, res: Response, next: NextFunction) => { - const headers = { - 'X-Gu-Backend-App': backendApp, - 'X-Gu-Backend-App-Target-Group': 'dev', - }; - - for (const [key, value] of Object.entries(headers)) { - res.setHeader(key, value); - } - - next(); -}); +renderer.use(responseHeaderMiddleware); renderer.get('/Article/*url', handleArticle); renderer.get('/Interactive/*url', handleInteractive); diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts index d5016b4a5e5..62b75a3b639 100644 --- a/dotcom-rendering/src/server/server.prod.ts +++ b/dotcom-rendering/src/server/server.prod.ts @@ -1,10 +1,5 @@ import compression from 'compression'; -import type { - ErrorRequestHandler, - NextFunction, - Request, - Response, -} from 'express'; +import type { ErrorRequestHandler, Request, Response } from 'express'; import express from 'express'; import { NotRenderableInDCR } from '../lib/errors/not-renderable-in-dcr'; import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web'; @@ -33,17 +28,11 @@ import { } from './handler.sportDataPage'; import { handleAppsThrasher } from './handler.thrasher.apps'; import { recordBaselineCloudWatchMetrics } from './lib/aws/metrics-baseline'; +import { responseHeaderMiddleware } from './lib/header-middleware'; import { logger } from './lib/logging'; import { requestLoggerMiddleware } from './lib/logging-middleware'; import { recordError } from './lib/logging-store'; -// TODO extract this, and other places that read env vars, into a `config` object? -// See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-environment-variables.html -const runningInECS = - process.env.AWS_EXECUTION_ENV?.startsWith('AWS_ECS_') === true; - -const backendApp = process.env.GU_APP ?? 'unknown'; - export const prodServer = (): void => { logger.info('dotcom-rendering is GO.'); @@ -52,19 +41,7 @@ export const prodServer = (): void => { app.use(express.json({ limit: '50mb' })); app.use(requestLoggerMiddleware); app.use(compression()); - - app.use((req: Request, res: Response, next: NextFunction) => { - const headers = { - 'X-Gu-Backend-App': backendApp, - 'X-Gu-Backend-App-Target-Group': runningInECS ? 'ecs' : 'ec2', - }; - - for (const [key, value] of Object.entries(headers)) { - res.setHeader(key, value); - } - - next(); - }); + app.use(responseHeaderMiddleware); app.get('/_healthcheck', (req: Request, res: Response) => { res.status(200).send('OKAY'); From dd937f99fae7bf5295679cbc85c5ab1e03f959ec Mon Sep 17 00:00:00 2001 From: Akash Askoolum Date: Wed, 15 Jul 2026 16:43:18 +0100 Subject: [PATCH 3/4] refactor: Use consistent fallback for GU_APP Other places in the repository fallback to "rendering". Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dotcom-rendering/src/server/lib/header-middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/server/lib/header-middleware.ts b/dotcom-rendering/src/server/lib/header-middleware.ts index 5293eacef62..4a3ca8f6294 100644 --- a/dotcom-rendering/src/server/lib/header-middleware.ts +++ b/dotcom-rendering/src/server/lib/header-middleware.ts @@ -11,7 +11,7 @@ const getTargetGroupHeaderValue = () => { return 'dev'; }; -const backendApp = process.env.GU_APP ?? 'dev'; +const backendApp = process.env.GU_APP ?? 'rendering'; const targetGroup = getTargetGroupHeaderValue(); /** From 43ec37409e2f6a8647165742fc450b1cb3177592 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 16 Jul 2026 12:37:17 +0100 Subject: [PATCH 4/4] refactor: Reduce module level state --- .../src/server/lib/header-middleware.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/server/lib/header-middleware.ts b/dotcom-rendering/src/server/lib/header-middleware.ts index 4a3ca8f6294..bc37b2446d3 100644 --- a/dotcom-rendering/src/server/lib/header-middleware.ts +++ b/dotcom-rendering/src/server/lib/header-middleware.ts @@ -11,8 +11,15 @@ const getTargetGroupHeaderValue = () => { return 'dev'; }; -const backendApp = process.env.GU_APP ?? 'rendering'; -const targetGroup = getTargetGroupHeaderValue(); +const getHeaders: () => Record = () => { + const backendApp = process.env.GU_APP ?? 'rendering'; + const targetGroup = getTargetGroupHeaderValue(); + + return { + 'X-Gu-Backend-App': backendApp, + 'X-Gu-Backend-App-Target-Group': targetGroup, + }; +}; /** * Middleware to add response headers useful for debugging. @@ -24,10 +31,7 @@ export const responseHeaderMiddleware: RequestHandler = ( res: Response, next: NextFunction, ) => { - const headers = { - 'X-Gu-Backend-App': backendApp, - 'X-Gu-Backend-App-Target-Group': targetGroup, - }; + const headers = getHeaders(); for (const [key, value] of Object.entries(headers)) { res.setHeader(key, value);