diff --git a/.changeset/observability-nested-null-validation.md b/.changeset/observability-nested-null-validation.md new file mode 100644 index 00000000000..1b47c408a0b --- /dev/null +++ b/.changeset/observability-nested-null-validation.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/workers-utils": patch +--- + +Report a null `observability.logs`/`observability.traces` as a config error instead of crashing + +`{ "observability": { "enabled": true, "logs": null } }` crashed with `TypeError: Cannot read properties of null (reading 'enabled')` instead of producing a validation error. Because `typeof null === "object"`, null passed the type check for these nested objects and then the per-property checks dereferenced it. + +Null is now rejected with a normal config error, matching how a null top-level `observability` is already handled. diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 0f17322d254..845f9c434a3 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -6416,7 +6416,14 @@ const validateObservability: ValidatorFn = (diagnostics, field, value) => { /** * Validate the optional nested logs configuration */ - if (typeof val.logs === "object") { + if (val.logs === null) { + // `typeof null === "object"`, so the check above accepts null; reject it + // here rather than crashing on the nested property checks below. + diagnostics.errors.push( + `Expected "${field}.logs" to be of type object but got null.` + ); + isValid = false; + } else if (typeof val.logs === "object") { isValid = validateOptionalProperty( diagnostics, @@ -6474,7 +6481,14 @@ const validateObservability: ValidatorFn = (diagnostics, field, value) => { /** * Validate the optional nested traces configuration */ - if (typeof val.traces === "object") { + if (val.traces === null) { + // `typeof null === "object"`, so the check above accepts null; reject it + // here rather than crashing on the nested property checks below. + diagnostics.errors.push( + `Expected "${field}.traces" to be of type object but got null.` + ); + isValid = false; + } else if (typeof val.traces === "object") { isValid = validateOptionalProperty( diagnostics, diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index fdc773a571a..5d52be136ca 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -10226,6 +10226,42 @@ describe("normalizeAndValidateConfig()", () => { `); }); + it("should error if observability.logs is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + observability: { enabled: true, logs: null }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Expected "observability.logs" to be of type object but got null." + `); + }); + + it("should error if observability.traces is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + observability: { enabled: true, traces: null }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Expected "observability.traces" to be of type object but got null." + `); + }); + it("should not warn on full observability config", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( {