From 96c490139059e42c1d7adeecd25a3c78c46e6c41 Mon Sep 17 00:00:00 2001 From: ReinerBRO <593493640@qq.com> Date: Thu, 19 Mar 2026 03:25:47 +0800 Subject: [PATCH] fix(config): allow null header values in headers rules --- source/utilities/config.ts | 38 ++++++++++++++++++- .../config/header-removal/serve.json | 23 +++++++++++ tests/config.test.ts | 19 +++++++++- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/__fixtures__/config/header-removal/serve.json diff --git a/source/utilities/config.ts b/source/utilities/config.ts index db2e3c85..889a64fd 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -14,6 +14,42 @@ import { logger } from './logger.js'; import type { ErrorObject } from 'ajv'; import type { Configuration, Options, NodeError } from '../types.js'; +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +const schemaWithNullableHeaderValue = (() => { + const staticSchema = JSON.parse(JSON.stringify(schema)) as Record< + string, + unknown + >; + const properties = staticSchema.properties; + if (!isRecord(properties)) return staticSchema; + + const headers = properties.headers; + if (!isRecord(headers)) return staticSchema; + + const headerItems = headers.items; + if (!isRecord(headerItems)) return staticSchema; + + const headerProperties = headerItems.properties; + if (!isRecord(headerProperties)) return staticSchema; + + const headerEntries = headerProperties.headers; + if (!isRecord(headerEntries)) return staticSchema; + + const headerEntryItems = headerEntries.items; + if (!isRecord(headerEntryItems)) return staticSchema; + + const headerEntryProperties = headerEntryItems.properties; + if (!isRecord(headerEntryProperties)) return staticSchema; + + const headerValue = headerEntryProperties.value; + if (!isRecord(headerValue)) return staticSchema; + + headerValue.type = ['string', 'null']; + return staticSchema; +})(); + /** * Parses and returns a configuration object from the designated locations. * @@ -122,7 +158,7 @@ export const loadConfiguration = async ( // If the configuration isn't empty, validate it against the AJV schema. if (Object.keys(config).length !== 0) { const ajv = new Ajv({ allowUnionTypes: true }); - const validate = ajv.compile(schema as object); + const validate = ajv.compile(schemaWithNullableHeaderValue as object); if (!validate(config) && validate.errors) { const defaultMessage = 'The configuration you provided is invalid:'; diff --git a/tests/__fixtures__/config/header-removal/serve.json b/tests/__fixtures__/config/header-removal/serve.json new file mode 100644 index 00000000..1af3b874 --- /dev/null +++ b/tests/__fixtures__/config/header-removal/serve.json @@ -0,0 +1,23 @@ +{ + "public": "app/", + "headers": [ + { + "source": "**", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=86400" + } + ] + }, + { + "source": "service-worker.js", + "headers": [ + { + "key": "Cache-Control", + "value": null + } + ] + } + ] +} diff --git a/tests/config.test.ts b/tests/config.test.ts index 3240d1f0..1ac48c32 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -11,7 +11,7 @@ import { Options } from '../source/types.js'; const fixtures = 'tests/__fixtures__/config/'; // A helper function to load the configuration for a certain fixture. const loadConfig = ( - name: 'valid' | 'invalid' | 'non-existent' | 'deprecated', + name: 'valid' | 'invalid' | 'non-existent' | 'deprecated' | 'header-removal', args?: Partial = {}, ) => loadConfiguration(process.cwd(), `${fixtures}/${name}`, args); @@ -65,4 +65,21 @@ describe('utilities/config', () => { expect.stringContaining('deprecated'), ); }); + + // `serve-handler` supports `null` to remove previously defined headers. + // This should pass config validation as well. + test('accept null header values for header removal rules', async () => { + const configuration = await loadConfig('header-removal'); + + expect(configuration.headers).toEqual([ + { + source: '**', + headers: [{ key: 'Cache-Control', value: 'max-age=86400' }], + }, + { + source: 'service-worker.js', + headers: [{ key: 'Cache-Control', value: null }], + }, + ]); + }); });