Skip to content

Commit 9cc8770

Browse files
committed
guard precomputed ETag reuse by context
Validate caller-supplied previous configurations before sending their ETag or reusing them on a 304 response. Environment: Datadog workspace
1 parent 0ae395b commit 9cc8770

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

packages/browser/src/transport/fetchConfiguration.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,15 @@ export async function fetchPrecomputedConfiguration(
8989
options: PrecomputedConfigurationFetchOptions
9090
): Promise<FlagsConfiguration> {
9191
const url = buildConfigurationUrl(options, 'precomputed')
92+
const previousConfiguration = configMatchesContext(options.previousConfiguration, options.context)
93+
? options.previousConfiguration
94+
: undefined
9295
const defaultHeaders = buildConfigurationHeaders(
9396
options,
9497
{
9598
'Content-Type': 'application/vnd.api+json',
96-
...(options.previousConfiguration?.precomputed?.etag && {
97-
'If-None-Match': options.previousConfiguration.precomputed.etag,
99+
...(previousConfiguration?.precomputed?.etag && {
100+
'If-None-Match': previousConfiguration.precomputed.etag,
98101
}),
99102
},
100103
'precomputed'
@@ -128,8 +131,8 @@ export async function fetchPrecomputedConfiguration(
128131
},
129132
}),
130133
})
131-
if (response.status === 304 && options.previousConfiguration?.precomputed) {
132-
return { precomputed: options.previousConfiguration.precomputed }
134+
if (response.status === 304 && previousConfiguration?.precomputed) {
135+
return { precomputed: previousConfiguration.precomputed }
133136
}
134137
if (!response.ok) {
135138
const errorMessage = await getErrorMessage(response)
@@ -156,7 +159,7 @@ export function createFlagsConfigurationFetcher(initConfiguration: FlaggingInitC
156159
...initConfiguration,
157160
context,
158161
signal,
159-
previousConfiguration: configMatchesContext(previousConfiguration, context) ? previousConfiguration : undefined,
162+
previousConfiguration,
160163
})
161164
previousConfiguration = configuration
162165
return configuration

packages/browser/test/transport/fetchConfiguration.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,42 @@ describe('createFlagsConfigurationFetcher', () => {
384384
)
385385
})
386386

387+
it('does not reuse a previous precomputed configuration for a different context', async () => {
388+
const contextA = { targetingKey: 'user-a' }
389+
const contextB = { targetingKey: 'user-b' }
390+
const options = { ...baseConfig, flaggingProxy: 'https://proxy.example.com', context: contextA }
391+
mockFetch.mockResolvedValueOnce({
392+
ok: true,
393+
status: 200,
394+
headers: new Headers({ etag: 'context-a-etag' }),
395+
json: jest.fn().mockResolvedValue({ assignments: 'user-a' }),
396+
})
397+
const previousConfiguration = await fetchPrecomputedConfiguration(options)
398+
mockFetch.mockImplementationOnce((_url, init) => {
399+
const headers = init?.headers as Record<string, string>
400+
if (headers['If-None-Match'] === 'context-a-etag') {
401+
return Promise.resolve({ ok: false, status: 304, headers: new Headers() })
402+
}
403+
return Promise.resolve({
404+
ok: true,
405+
status: 200,
406+
headers: new Headers(),
407+
json: jest.fn().mockResolvedValue({ assignments: 'user-b' }),
408+
})
409+
})
410+
411+
const result = await fetchPrecomputedConfiguration({
412+
...options,
413+
context: contextB,
414+
previousConfiguration,
415+
})
416+
417+
expect(result.precomputed).toMatchObject({
418+
context: contextB,
419+
response: { assignments: 'user-b' },
420+
})
421+
})
422+
387423
it('decodes a rules configuration response', async () => {
388424
const bytes = Uint8Array.from(Buffer.from(rulesWire.rules.response, 'base64'))
389425
mockFetch.mockResolvedValue({

0 commit comments

Comments
 (0)