diff --git a/lib/project_config/config_manager_factory.browser.spec.ts b/lib/project_config/config_manager_factory.browser.spec.ts index 9dfa7bced..de6f8c78c 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,6 +78,7 @@ describe('createPollingConfigManager', () => { autoUpdate: true, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Test-Header': 'test-value' }, cache: getMockSyncCache(), }; 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..23d757f6c 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,6 +78,7 @@ describe('createPollingConfigManager', () => { autoUpdate: false, urlTemplate: 'urlTemplate', datafileAccessToken: 'datafileAccessToken', + customHeaders: { 'X-Test-Header': 'test-value' }, cache: getMockSyncCache(), }; 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..384039745 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(), }; diff --git a/lib/project_config/config_manager_factory.react_native.ts b/lib/project_config/config_manager_factory.react_native.ts index 8ea480595..e30a565ca 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,12 +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 { OpaqueConfigManager } from "./config_manager_factory"; +import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; +import { getOpaquePollingConfigManager, PollingConfigManagerConfig, OpaqueConfigManager } from "./config_manager_factory"; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { 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..7e928b8f8 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; }