From 472792bfc53c5df260ee9e7227b8bbf3c8bd8dfc Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 13 Aug 2026 09:27:57 +0100 Subject: [PATCH 1/4] Defensive coding of resolveLanguage to prevent 500s --- src/server/utils/utils.js | 9 ++++++-- src/server/utils/utils.test.js | 40 +++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/server/utils/utils.js b/src/server/utils/utils.js index 823078f52..10674a641 100644 --- a/src/server/utils/utils.js +++ b/src/server/utils/utils.js @@ -43,11 +43,16 @@ export function resolveLanguage(query, yar) { query ??= {} - if (yar && 'language' in query) { + // yar.id is only set once yar's session has been initialised (onPreAuth), + // so calling yar.get/set beforehand (e.g. for a 404 raised pre-routing) + // would throw as its internal store is still null + const sessionReady = Boolean(yar?.id) + + if (sessionReady && yar && 'language' in query) { yar.set('language', query.language) } - return yar?.get('language') ?? defaultLang + return (sessionReady && yar ? yar.get('language') : null) ?? defaultLang } /** diff --git a/src/server/utils/utils.test.js b/src/server/utils/utils.test.js index 23d528c56..f6b674428 100644 --- a/src/server/utils/utils.test.js +++ b/src/server/utils/utils.test.js @@ -3,7 +3,8 @@ import { getTraceId } from '@defra/hapi-tracing' import { config } from '~/src/config/index.js' import { applyTraceHeaders, - getFeedbackFormLink + getFeedbackFormLink, + resolveLanguage } from '~/src/server/utils/utils.js' jest.mock('@defra/hapi-tracing') @@ -64,4 +65,41 @@ describe('utils', () => { }) }) }) + + describe('resolveLanguage', () => { + const mockYarStore = /** @type {Record} */ ({}) + /** + * @param {string} name + * @param {string} value + */ + function yarSet(name, value) { + mockYarStore[name] = value + } + /** + * @param {string} name + */ + function yarGet(name) { + return mockYarStore[name] + } + + it('should return default language if yar session not properly constructed yet', () => { + expect(resolveLanguage({}, undefined)).toBe('en-GB') + }) + + it('should return specified language if yar session up and language passed as query param', () => { + // @ts-expect-error - partial mock of methods + expect( + resolveLanguage( + { language: 'cy' }, + { id: 'session-id', set: yarSet, get: yarGet } + ) + ).toBe('cy') + + // Should retrieve session value when language not passed + // @ts-expect-error - partial mock of methods + expect( + resolveLanguage({}, { id: 'session-id', set: yarSet, get: yarGet }) + ).toBe('cy') + }) + }) }) From c4eecec14f217a6afbc149348835460aaba61cd4 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 13 Aug 2026 09:28:42 +0100 Subject: [PATCH 2/4] Lint fix --- src/server/utils/utils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/utils/utils.test.js b/src/server/utils/utils.test.js index f6b674428..ae787296b 100644 --- a/src/server/utils/utils.test.js +++ b/src/server/utils/utils.test.js @@ -87,17 +87,17 @@ describe('utils', () => { }) it('should return specified language if yar session up and language passed as query param', () => { - // @ts-expect-error - partial mock of methods expect( resolveLanguage( { language: 'cy' }, + // @ts-expect-error - partial mock of methods { id: 'session-id', set: yarSet, get: yarGet } ) ).toBe('cy') // Should retrieve session value when language not passed - // @ts-expect-error - partial mock of methods expect( + // @ts-expect-error - partial mock of methods resolveLanguage({}, { id: 'session-id', set: yarSet, get: yarGet }) ).toBe('cy') }) From f5c3cfefcbcef6f70a7fb5683f820e724da456e6 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 13 Aug 2026 09:39:14 +0100 Subject: [PATCH 3/4] Fixed test --- src/server/i18n/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/i18n/index.test.ts b/src/server/i18n/index.test.ts index c52ce88bf..550215736 100644 --- a/src/server/i18n/index.test.ts +++ b/src/server/i18n/index.test.ts @@ -36,6 +36,7 @@ describe('Runner i18n', () => { it('returns the language set in the session', () => { const blankRequest = { yar: { + id: 'session-id', get: jest.fn().mockReturnValue('cy') } } as unknown as Request @@ -46,6 +47,7 @@ describe('Runner i18n', () => { const mockYarSet = jest.fn() const blankRequest = { yar: { + id: 'session-id', get: jest.fn(), set: mockYarSet }, From fa053ca5cc230165d1efb0ddc29e764367285da6 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 13 Aug 2026 10:02:10 +0100 Subject: [PATCH 4/4] After review comment --- src/server/utils/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/utils/utils.js b/src/server/utils/utils.js index 10674a641..feea0847e 100644 --- a/src/server/utils/utils.js +++ b/src/server/utils/utils.js @@ -46,13 +46,13 @@ export function resolveLanguage(query, yar) { // yar.id is only set once yar's session has been initialised (onPreAuth), // so calling yar.get/set beforehand (e.g. for a 404 raised pre-routing) // would throw as its internal store is still null - const sessionReady = Boolean(yar?.id) + const sessionReady = yar && Boolean(yar.id) - if (sessionReady && yar && 'language' in query) { + if (sessionReady && 'language' in query) { yar.set('language', query.language) } - return (sessionReady && yar ? yar.get('language') : null) ?? defaultLang + return (sessionReady ? yar.get('language') : null) ?? defaultLang } /**