From aa7d25cca470b74b0ee9d54b7871b9cbdf6ef8f0 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 14 Nov 2025 08:14:40 +0600 Subject: [PATCH 1/4] [FSSDK-12026] add customHeaders option to polling config manager --- .../config_manager_factory.browser.spec.ts | 22 +++++- .../config_manager_factory.browser.ts | 5 +- .../config_manager_factory.node.spec.ts | 22 +++++- .../config_manager_factory.node.ts | 6 +- ...onfig_manager_factory.react_native.spec.ts | 22 +++++- .../config_manager_factory.react_native.ts | 7 +- .../config_manager_factory.spec.ts | 2 + lib/project_config/config_manager_factory.ts | 2 + lib/project_config/datafile_manager.ts | 1 + .../polling_datafile_manager.spec.ts | 79 +++++++++++++++++++ .../polling_datafile_manager.ts | 8 +- 11 files changed, 161 insertions(+), 15 deletions(-) diff --git a/lib/project_config/config_manager_factory.browser.spec.ts b/lib/project_config/config_manager_factory.browser.spec.ts index 9dfa7bced..abb6c438d 100644 --- a/lib/project_config/config_manager_factory.browser.spec.ts +++ b/lib/project_config/config_manager_factory.browser.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,10 +78,30 @@ describe('createPollingConfigManager', () => { autoUpdate: true, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Test-Header': 'test-value' }, cache: getMockSyncCache(), }; const projectConfigManager = createPollingProjectConfigManager(config); expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); + + it('passes customHeaders through to the underlying config manager', () => { + const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'X-Another-Header': 'another-value', + }; + + const config = { + sdkKey: 'sdkKey', + customHeaders, + }; + + createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( + expect.objectContaining({ + customHeaders, + }) + ); + }); }); diff --git a/lib/project_config/config_manager_factory.browser.ts b/lib/project_config/config_manager_factory.browser.ts index 0a96affd5..17741acb2 100644 --- a/lib/project_config/config_manager_factory.browser.ts +++ b/lib/project_config/config_manager_factory.browser.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ * limitations under the License. */ -import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; -import { ProjectConfigManager } from './project_config_manager'; +import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { diff --git a/lib/project_config/config_manager_factory.node.spec.ts b/lib/project_config/config_manager_factory.node.spec.ts index c0631a63b..3a0823647 100644 --- a/lib/project_config/config_manager_factory.node.spec.ts +++ b/lib/project_config/config_manager_factory.node.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,10 +78,30 @@ describe('createPollingConfigManager', () => { autoUpdate: false, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Test-Header': 'test-value' }, cache: getMockSyncCache(), }; const projectConfigManager = createPollingProjectConfigManager(config); expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); + + it('passes customHeaders through to the underlying config manager', () => { + const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'X-Another-Header': 'another-value', + }; + + const config = { + sdkKey: 'sdkKey', + customHeaders, + }; + + createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( + expect.objectContaining({ + customHeaders, + }) + ); + }); }); diff --git a/lib/project_config/config_manager_factory.node.ts b/lib/project_config/config_manager_factory.node.ts index 58ac126bc..8e063e347 100644 --- a/lib/project_config/config_manager_factory.node.ts +++ b/lib/project_config/config_manager_factory.node.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,8 @@ * limitations under the License. */ -import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; import { NodeRequestHandler } from "../utils/http_request_handler/request_handler.node"; -import { ProjectConfigManager } from "./project_config_manager"; -import { DEFAULT_URL_TEMPLATE, DEFAULT_AUTHENTICATED_URL_TEMPLATE } from './constant'; +import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { diff --git a/lib/project_config/config_manager_factory.react_native.spec.ts b/lib/project_config/config_manager_factory.react_native.spec.ts index 52411861d..cf196db3d 100644 --- a/lib/project_config/config_manager_factory.react_native.spec.ts +++ b/lib/project_config/config_manager_factory.react_native.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -129,6 +129,7 @@ describe('createPollingConfigManager', () => { autoUpdate: false, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Test-Header': 'test-value' }, cache: getMockSyncCache(), }; @@ -137,6 +138,25 @@ describe('createPollingConfigManager', () => { expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); + it('passes customHeaders through to the underlying config manager', () => { + const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'X-Another-Header': 'another-value', + }; + + const config = { + sdkKey: 'sdkKey', + customHeaders, + }; + + createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( + expect.objectContaining({ + customHeaders, + }) + ); + }); + it('Should not throw error if a cache is present in the config, and async storage is not available', async () => { isAsyncStorageAvailable = false; const config = { diff --git a/lib/project_config/config_manager_factory.react_native.ts b/lib/project_config/config_manager_factory.react_native.ts index 8ea480595..962960e47 100644 --- a/lib/project_config/config_manager_factory.react_native.ts +++ b/lib/project_config/config_manager_factory.react_native.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,9 @@ * limitations under the License. */ -import { getOpaquePollingConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; -import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; -import { ProjectConfigManager } from "./project_config_manager"; import { AsyncStorageCache } from "../utils/cache/async_storage_cache.react_native"; +import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; +import { getOpaquePollingConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; import { OpaqueConfigManager } from "./config_manager_factory"; diff --git a/lib/project_config/config_manager_factory.spec.ts b/lib/project_config/config_manager_factory.spec.ts index 7def4f9a8..6f69d007d 100644 --- a/lib/project_config/config_manager_factory.spec.ts +++ b/lib/project_config/config_manager_factory.spec.ts @@ -159,6 +159,7 @@ describe('getPollingConfigManager', () => { autoUpdate: true, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Custom-Header': 'custom-value' }, cache: getMockSyncCache(), }; @@ -171,6 +172,7 @@ describe('getPollingConfigManager', () => { autoUpdate: config.autoUpdate, urlTemplate: config.urlTemplate, datafileAccessToken: config.datafileAccessToken, + customHeaders: config.customHeaders, requestHandler: config.requestHandler, repeater: MockIntervalRepeater.mock.instances[0], cache: config.cache, diff --git a/lib/project_config/config_manager_factory.ts b/lib/project_config/config_manager_factory.ts index e7d21aeea..3224d4f91 100644 --- a/lib/project_config/config_manager_factory.ts +++ b/lib/project_config/config_manager_factory.ts @@ -56,6 +56,7 @@ export type PollingConfigManagerConfig = { updateInterval?: number; urlTemplate?: string; datafileAccessToken?: string; + customHeaders?: Record; cache?: Store; }; @@ -88,6 +89,7 @@ export const getPollingConfigManager = ( autoUpdate: opt.autoUpdate, urlTemplate: opt.urlTemplate, datafileAccessToken: opt.datafileAccessToken, + customHeaders: opt.customHeaders, requestHandler: opt.requestHandler, cache: opt.cache, repeater, diff --git a/lib/project_config/datafile_manager.ts b/lib/project_config/datafile_manager.ts index c5765a539..b7c724113 100644 --- a/lib/project_config/datafile_manager.ts +++ b/lib/project_config/datafile_manager.ts @@ -33,6 +33,7 @@ export type DatafileManagerConfig = { urlTemplate?: string; cache?: Store; datafileAccessToken?: string; + customHeaders?: Record; initRetry?: number; repeater: Repeater; logger?: LoggerFacade; diff --git a/lib/project_config/polling_datafile_manager.spec.ts b/lib/project_config/polling_datafile_manager.spec.ts index 921ab2a93..b87416186 100644 --- a/lib/project_config/polling_datafile_manager.spec.ts +++ b/lib/project_config/polling_datafile_manager.spec.ts @@ -789,6 +789,85 @@ describe('PollingDatafileManager', () => { expect(requestHandler.makeRequest.mock.calls[0][1].Authorization).toBe('Bearer token123'); }); + it('sends customHeaders in the request headers', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'X-Another-Header': 'another-value', + }; + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + customHeaders, + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + const sentHeaders = requestHandler.makeRequest.mock.calls[0][1]; + expect(sentHeaders['X-Custom-Header']).toBe('custom-value'); + expect(sentHeaders['X-Another-Header']).toBe('another-value'); + }); + + it('merges customHeaders with other headers (access token and if-modified-since)', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + + // First request to set up last-modified header + const mockResponse1 = getMockAbortableRequest(Promise.resolve({ + statusCode: 200, + body: '{"foo": "bar"}', + headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT' } + })); + + // Second request to test all headers together + const mockResponse2 = getMockAbortableRequest(Promise.resolve({ + statusCode: 304, + body: '', + headers: {} + })); + + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2); + + const customHeaders = { + 'X-Custom-Header': 'custom-value', + }; + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + datafileAccessToken: 'token123', + customHeaders, + autoUpdate: true, + }); + + manager.start(); + + // First request + await repeater.execute(0); + + // Second request should have all headers + await repeater.execute(0); + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(2); + + // Check second request headers include custom, auth, and if-modified-since + const secondRequestHeaders = requestHandler.makeRequest.mock.calls[1][1]; + expect(secondRequestHeaders['X-Custom-Header']).toBe('custom-value'); + expect(secondRequestHeaders['Authorization']).toBe('Bearer token123'); + expect(secondRequestHeaders['if-modified-since']).toBe('Fri, 08 Mar 2019 18:57:17 GMT'); + }); + it('uses the provided urlTemplate', async () => { const repeater = getMockRepeater(); const requestHandler = getMockRequestHandler(); diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index ba8e70139..50dc68c3f 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -56,6 +56,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag private cache?: Store; private sdkKey: string; private datafileAccessToken?: string; + private customHeaders?: Record; constructor(config: DatafileManagerConfig) { super(config.startupLogs); @@ -63,6 +64,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag autoUpdate = false, sdkKey, datafileAccessToken, + customHeaders, urlTemplate, cache, initRetry, @@ -74,6 +76,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag this.cacheKey = 'opt-datafile-' + sdkKey; this.sdkKey = sdkKey; this.datafileAccessToken = datafileAccessToken; + this.customHeaders = customHeaders; this.requestHandler = requestHandler; this.emitter = new EventEmitter(); this.autoUpdate = autoUpdate; @@ -194,7 +197,10 @@ export class PollingDatafileManager extends BaseService implements DatafileManag } private makeDatafileRequest(): AbortableRequest { - const headers: Headers = {}; + const headers: Headers = { + ...this.customHeaders, + }; + if (this.lastResponseLastModified) { headers['if-modified-since'] = this.lastResponseLastModified; } From e7ad72e6477a2bf06fbfa803fcd83abf39168ee8 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 14 Nov 2025 08:19:59 +0600 Subject: [PATCH 2/4] rem --- .../config_manager_factory.browser.spec.ts | 19 ------------------- .../config_manager_factory.node.spec.ts | 19 ------------------- ...onfig_manager_factory.react_native.spec.ts | 12 ------------ 3 files changed, 50 deletions(-) diff --git a/lib/project_config/config_manager_factory.browser.spec.ts b/lib/project_config/config_manager_factory.browser.spec.ts index abb6c438d..de6f8c78c 100644 --- a/lib/project_config/config_manager_factory.browser.spec.ts +++ b/lib/project_config/config_manager_factory.browser.spec.ts @@ -85,23 +85,4 @@ describe('createPollingConfigManager', () => { const projectConfigManager = createPollingProjectConfigManager(config); expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); - - it('passes customHeaders through to the underlying config manager', () => { - const customHeaders = { - 'X-Custom-Header': 'custom-value', - 'X-Another-Header': 'another-value', - }; - - const config = { - sdkKey: 'sdkKey', - customHeaders, - }; - - createPollingProjectConfigManager(config); - expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( - expect.objectContaining({ - customHeaders, - }) - ); - }); }); diff --git a/lib/project_config/config_manager_factory.node.spec.ts b/lib/project_config/config_manager_factory.node.spec.ts index 3a0823647..23d757f6c 100644 --- a/lib/project_config/config_manager_factory.node.spec.ts +++ b/lib/project_config/config_manager_factory.node.spec.ts @@ -85,23 +85,4 @@ describe('createPollingConfigManager', () => { const projectConfigManager = createPollingProjectConfigManager(config); expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); - - it('passes customHeaders through to the underlying config manager', () => { - const customHeaders = { - 'X-Custom-Header': 'custom-value', - 'X-Another-Header': 'another-value', - }; - - const config = { - sdkKey: 'sdkKey', - customHeaders, - }; - - createPollingProjectConfigManager(config); - expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( - expect.objectContaining({ - customHeaders, - }) - ); - }); }); diff --git a/lib/project_config/config_manager_factory.react_native.spec.ts b/lib/project_config/config_manager_factory.react_native.spec.ts index cf196db3d..eb3407141 100644 --- a/lib/project_config/config_manager_factory.react_native.spec.ts +++ b/lib/project_config/config_manager_factory.react_native.spec.ts @@ -157,18 +157,6 @@ describe('createPollingConfigManager', () => { ); }); - it('Should not throw error if a cache is present in the config, and async storage is not available', async () => { - isAsyncStorageAvailable = false; - const config = { - sdkKey: 'sdkKey', - requestHandler: { makeRequest: vi.fn() }, - cache: getMockSyncCache(), - }; - - expect(() => createPollingProjectConfigManager(config)).not.toThrow(); - isAsyncStorageAvailable = true; - }); - it('should throw an error if cache is not present in the config, and async storage is not available', async () => { isAsyncStorageAvailable = false; From fd588c64ccab5f05adfc2bf25a55b7a76fa26949 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 14 Nov 2025 08:21:19 +0600 Subject: [PATCH 3/4] fix --- ...onfig_manager_factory.react_native.spec.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/project_config/config_manager_factory.react_native.spec.ts b/lib/project_config/config_manager_factory.react_native.spec.ts index eb3407141..384039745 100644 --- a/lib/project_config/config_manager_factory.react_native.spec.ts +++ b/lib/project_config/config_manager_factory.react_native.spec.ts @@ -138,23 +138,16 @@ describe('createPollingConfigManager', () => { expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); }); - it('passes customHeaders through to the underlying config manager', () => { - const customHeaders = { - 'X-Custom-Header': 'custom-value', - 'X-Another-Header': 'another-value', - }; - + it('Should not throw error if a cache is present in the config, and async storage is not available', async () => { + isAsyncStorageAvailable = false; const config = { sdkKey: 'sdkKey', - customHeaders, + requestHandler: { makeRequest: vi.fn() }, + cache: getMockSyncCache(), }; - createPollingProjectConfigManager(config); - expect(mockGetOpaquePollingConfigManager).toHaveBeenCalledWith( - expect.objectContaining({ - customHeaders, - }) - ); + expect(() => createPollingProjectConfigManager(config)).not.toThrow(); + isAsyncStorageAvailable = true; }); it('should throw an error if cache is not present in the config, and async storage is not available', async () => { From 9a6b319970de22619320908f71fdc93be14d215f Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 14 Nov 2025 18:38:29 +0600 Subject: [PATCH 4/4] up --- lib/project_config/config_manager_factory.react_native.ts | 4 +--- lib/project_config/polling_datafile_manager.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/project_config/config_manager_factory.react_native.ts b/lib/project_config/config_manager_factory.react_native.ts index 962960e47..e30a565ca 100644 --- a/lib/project_config/config_manager_factory.react_native.ts +++ b/lib/project_config/config_manager_factory.react_native.ts @@ -16,9 +16,7 @@ import { AsyncStorageCache } from "../utils/cache/async_storage_cache.react_native"; import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; -import { getOpaquePollingConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; - -import { OpaqueConfigManager } from "./config_manager_factory"; +import { getOpaquePollingConfigManager, PollingConfigManagerConfig, OpaqueConfigManager } from "./config_manager_factory"; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index 50dc68c3f..7e928b8f8 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -200,7 +200,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag const headers: Headers = { ...this.customHeaders, }; - + if (this.lastResponseLastModified) { headers['if-modified-since'] = this.lastResponseLastModified; }