Skip to content

Fix headersSchema case sensitivity for camelCase property names#2946

Open
nick-inkeep wants to merge 5 commits intomainfrom
feat/normalize-header-schema-case
Open

Fix headersSchema case sensitivity for camelCase property names#2946
nick-inkeep wants to merge 5 commits intomainfrom
feat/normalize-header-schema-case

Conversation

@nick-inkeep
Copy link
Copy Markdown
Collaborator

@nick-inkeep nick-inkeep commented Apr 1, 2026

Summary

  • Normalize headersSchema property names and required entries to lowercase before AJV validation and key filtering
  • Fixes 500 "Context validation failed" error when customers define camelCase properties (e.g., mcpToken) that don't match lowercased HTTP header keys (mcptoken)
  • HTTP headers are case-insensitive per RFC 7230/9110 and normalized to lowercase by the Fetch API — the schema should match

Changes

validation.ts

  • Extract normalizeSchemaKeysToLowercase() — recursively lowercases JSON Schema property names and required entries
  • Applied in validateHttpRequestHeaders() and filterContextToSchemaKeys() only (header-specific call sites)
  • NOT applied in the shared getCachedValidator() — that function is also used by ContextFetcher.validateResponseWithJsonSchema() for API response schemas where property names are case-sensitive

contextValidation.headers.test.ts

  • 4 new test cases: camelCase properties, camelCase required entries, mixed case properties, camelCase filtering

Other

  • Changeset for agents-api (patch)
  • Reverted unrelated auto-generated evaluations.mdx change

Root cause

contextValidationMiddleware correctly lowercases incoming header keys (key.toLowerCase()), but the headersSchema property names were NOT lowercased before validation. AJV's property matching is case-sensitive, so { properties: { "mcpToken" } } didn't match data key "mcptoken". Additionally, filterContextToSchemaKeys iterates schema properties and checks key in data — also case-sensitive.

Blast radius

  • normalizeSchemaKeysToLowercase is scoped to header validation only
  • getCachedValidator() (shared by ContextFetcher for API response validation) is NOT affected — response schemas with camelCase properties (e.g., userId) continue to work correctly
  • Schema cache unaffected — cache key uses pre-normalization schema string

Test plan

  • camelCase schema property validates against lowercase header
  • camelCase required entry enforces correctly
  • Mixed case properties all normalize
  • Filtering returns only declared (lowercased) properties
  • All 46 existing context validation tests pass (no regression)
  • Typecheck passes
  • Local review: APPROVE (low risk, 0 blocking issues)

🤖 Generated with Claude Code

nick-inkeep and others added 3 commits April 1, 2026 01:29
…ation

HTTP header names are case-insensitive (RFC 7230/9110) and are normalized
to lowercase by the Fetch API. When customers defined headersSchema with
camelCase properties (e.g., mcpToken), AJV validation failed because the
schema expected "mcpToken" but the data had "mcptoken" — resulting in a
500 "Context validation failed" error.

Extract normalizeSchemaKeysToLowercase() that lowercases all JSON Schema
property names and required entries. Applied in both getCachedValidator()
(for AJV compilation) and filterContextToSchemaKeys() (for post-validation
filtering). Schema cache is unaffected — the cache key uses the original
schema string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Apr 1, 2026 9:01am
agents-docs Ready Ready Preview, Comment Apr 1, 2026 9:01am
agents-manage-ui Ready Ready Preview, Comment Apr 1, 2026 9:01am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 1, 2026

🦋 Changeset detected

Latest commit: 868187e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog bot commented Apr 1, 2026

TL;DR — Fixes a 500 error when customers define headersSchema with camelCase property names (e.g., mcpToken). HTTP headers are case-insensitive and normalized to lowercase by the Fetch API, but AJV validation was comparing against the original casing — causing a mismatch. The fix normalizes schema property names and required entries to lowercase before validation and filtering, scoped specifically to header schemas to avoid breaking case-sensitive API response validation.

Key changes

  • Add normalizeSchemaKeysToLowercase() to validation.ts — recursively lowercases JSON Schema property names and required entries, called at both the validation and post-validation filtering call sites (not in the shared getCachedValidator(), which is also used for API response validation where casing matters).
  • Add 4 test cases for camelCase header schema validation — covers camelCase properties, camelCase required entries, mixed-case properties, and filtering behavior.
  • Include spec and changeset — documents the root cause, proposed fix, and acceptance criteria; adds a patch changeset for @inkeep/agents-api.

Summary | 4 files | 5 commits | base: mainfeat/normalize-header-schema-case


Case-insensitive headersSchema validation

Before: headersSchema properties like mcpToken failed AJV validation against the lowercased header key mcptoken, producing a 500 "Context validation failed" error. Post-validation filtering also missed the key.
After: Both validateHttpRequestHeaders() and filterContextToSchemaKeys() normalize the schema to lowercase via normalizeSchemaKeysToLowercase() before use, so { properties: { mcpToken: ... } } correctly matches mcptoken in the request headers.

The new normalizeSchemaKeysToLowercase() function handles properties, required, and recursive schema keywords (items, oneOf, anyOf, allOf). It is a standalone function — separate from makeSchemaPermissive() — called only at header-specific call sites. An earlier iteration placed the normalization inside getCachedValidator(), but this was moved out because that validator is shared with ContextFetcher.validateResponseWithJsonSchema for API response validation, where lowercasing property names (e.g., userIduserid) would break case-sensitive responses.

Why normalize the schema instead of preserving original header casing?

HTTP/2 mandates lowercase headers, and the Fetch API (used by Hono) enforces this. The incoming data is always lowercase — the schema is the only side where casing can vary, making it the right normalization target.

validation.ts · contextValidation.headers.test.ts · SPEC.md

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity — normalizeSchemaKeysToLowercase is applied to ALL schemas, not just headers.

The fix for the header casing mismatch is correct in principle, but the normalization is placed inside getCachedValidator() — a shared utility also called from ContextFetcher.validateResponseWithJsonSchema() for response body schemas. Response bodies have case-sensitive JSON property names (e.g., userName, createdAt), so lowercasing them would cause validation mismatches. The normalization should be scoped to headers-only call sites. See inline comments for details and a minor cleanup note.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

}

const permissiveSchema = makeSchemaPermissive(schema);
const normalizedSchema = normalizeSchemaKeysToLowercase(schema);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCachedValidator is a shared function — it's also called from ContextFetcher.ts:416 via validationHelper() to validate HTTP response bodies against definition.responseSchema. Response body property names are case-sensitive JSON keys (e.g., userName, createdAt), not HTTP headers.

By placing normalizeSchemaKeysToLowercase here, every schema compiled through this path — not just headersSchema — gets its property names lowercased. This would silently break response schema validation for any schema with camelCase property names.

Fix: Move the normalization out of getCachedValidator and into the two headers-specific call sites:

  1. In validateHttpRequestHeaders (line 239), normalize before calling validationHelper
  2. In filterContextToSchemaKeys (line 202), normalize before calling filterByJsonSchema — this is already done correctly

This also eliminates the double-normalization: currently the schema is normalized once in getCachedValidator (for AJV) and again in filterContextToSchemaKeys (for filtering).

Comment on lines +62 to +63
const normalizedSchema = normalizeSchemaKeysToLowercase(schema);
const permissiveSchema = makeSchemaPermissive(normalizedSchema);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove normalization from getCachedValidator and keep it headers-only. The normalizeSchemaKeysToLowercase call should happen in validateHttpRequestHeaders before the schema reaches this function.

Suggested change
const normalizedSchema = normalizeSchemaKeysToLowercase(schema);
const permissiveSchema = makeSchemaPermissive(normalizedSchema);
const permissiveSchema = makeSchemaPermissive(schema);

}

const filteredHeaders = filterByJsonSchema(validatedContext, headersSchema);
const normalizedSchema = normalizeSchemaKeysToLowercase(headersSchema);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This normalization is correct for the filtering path. After moving normalization out of getCachedValidator, you could normalize once at the top of validateHttpRequestHeaders and pass the normalized schema to both validationHelper and filterContextToSchemaKeys, avoiding the redundant second normalization here.

url: '#get-evaluators-by-ids'
- depth: 2
title: Batch get agent scopes for evaluators
url: '#batch-get-agent-scopes-for-evaluators'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change (adding "Batch get agent scopes for evaluators" to the TOC and operations list) is unrelated to the header schema fix. The file header says This file was generated by Fumadocs. Do not edit this file directly. — this was likely regenerated as a side-effect of a build/rebase. Consider removing it from this PR to keep the diff focused.

@@ -0,0 +1,114 @@
# Normalize headersSchema Property Names to Lowercase
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the spec proposes extending makeSchemaPermissive() to include the lowercasing logic (lines 41–71), but the implementation chose a separate normalizeSchemaKeysToLowercase() function instead. The separate function is a better design (single responsibility), but the spec should be updated to match what was actually implemented — or removed from the PR if it was only a planning artifact.

Move normalizeSchemaKeysToLowercase() out of getCachedValidator() (shared
by all schemas) and into validateHttpRequestHeaders() (header-specific).

getCachedValidator is also used by ContextFetcher.validateResponseWithJsonSchema
for API response validation — lowercasing those property names would break
case-sensitive API responses (e.g., userId → userid).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(2) Total Issues | Risk: Low

🟡 Minor (1) 🟡

Inline Comments:

  • 🟡 Minor: contextValidation.headers.test.ts:181 Missing test for nested object schemas with camelCase (SPEC.md Test Case #5)

💭 Consider (1) 💭

Inline Comments:

  • 💭 Consider: validation.ts:78 Schema without explicit type: 'object' would skip normalization

🧹 While You're Here (1) 🧹

🧹 1) evaluations.mdx Unrelated auto-generated docs change

Issue: This file change (agents-docs/content/api-reference/(openapi)/evaluations.mdx) adds a new "Batch get agent scopes for evaluators" entry but is unrelated to the headersSchema case sensitivity fix.

Why: The file header says "This file was generated by Fumadocs. Do not edit this file directly." This appears to be incidental regeneration bundled with the PR. Consider separating into its own commit or PR for cleaner history.

Fix: Either remove from this PR (revert and let the next docs regeneration pick it up) or add a note in the PR description that this is an unrelated but legitimate auto-generated change.

Refs: evaluations.mdx diff

Discarded (2)
Location Issue Reason Discarded
validation.ts:81 Key collision when two properties differ only by case HTTP headers are case-insensitive per RFC 7230/9110 — customers defining conflicting case variants (e.g., McpToken and mcpToken) would be a user error. The "last wins" behavior is acceptable for this edge case.
contextValidation.headers.test.ts:182 Missing test for anyOf/oneOf composition schemas Composition schemas are uncommon for HTTP headers. The code handles them correctly and the existing tests provide sufficient coverage for the primary use case.
Reviewers (4)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-tests 4 0 0 0 1 0 2
pr-review-standards 2 0 0 0 1 0 1
pr-review-errors 0 0 0 0 0 0 0
pr-review-precision 0 0 0 0 0 0 0
Total 6 0 0 0 2 0 3

Note: Error handling and precision reviewers found no issues — the implementation is clean and well-targeted.


💡 APPROVE WITH SUGGESTIONS

Summary: This is a well-executed bug fix that correctly addresses the root cause of the case sensitivity mismatch between headersSchema property names and lowercased HTTP headers. The implementation is minimal, precisely targeted, and follows the existing patterns in the codebase. The new normalizeSchemaKeysToLowercase function is clean and handles JSON Schema composition keywords correctly. The test coverage is solid for the primary use cases. Minor suggestions above are optional improvements rather than blocking concerns. Nice work! 🎉


const normalized = { ...schema };

if (normalized.type === 'object') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Consider: Schema without explicit type: 'object'

Issue: This check requires type === 'object' explicitly, but JSON Schema allows properties without an explicit type declaration.

Why: A schema like { properties: { McpToken: { type: 'string' } } } (without type: 'object') would skip property normalization. However, the practical risk is low since all existing schemas in the codebase use type: 'object' explicitly, and this matches the pattern in makeSchemaPermissive() (line 118).

Fix: If you want to be maximally defensive, consider:

if (normalized.type === 'object' || normalized.properties)

Refs:

authtoken: 'bearer-xyz',
});
expect((result.validatedContext as any)?.['extra-header']).toBeUndefined();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Missing test for nested object schemas with camelCase

Issue: The normalizeSchemaKeysToLowercase function recursively processes nested object properties (line 82 in validation.ts), but no test verifies that deeply nested camelCase properties are correctly lowercased.

Why: This is explicitly called out in SPEC.md as Test Case #5 ("Nested object schemas: Schema with nested object properties using camelCase → properties lowercased at all levels"). While header values are typically strings, the recursive logic exists and should be tested for confidence.

Fix: Consider adding a test like:

it('should normalize nested object schema properties', async () => {
  const headersSchema = {
    type: 'object',
    properties: {
      authConfig: {
        type: 'object',
        properties: {
          accessToken: { type: 'string' },
        },
      },
    },
  };

  // Verify the schema compiles and validates without error
  const httpRequest: ParsedHttpRequest = {
    headers: {
      authconfig: 'some-value',
    },
  };

  const result = await validateHttpRequestHeaders(headersSchema, httpRequest);
  expect(result.valid).toBe(true);
});

Refs:

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) New Issues | Risk: Low | Delta Review

This review covers the 1 commit since the last automated review (56d3f74de91ad5b67b).


✅ Delta Assessment

The latest commit (91ad5b67b Fix: scope schema key normalization to header schemas only) correctly addresses the HIGH severity issue raised in the prior pullfrog review.

What changed:

- const normalizedSchema = normalizeSchemaKeysToLowercase(schema);
- const permissiveSchema = makeSchemaPermissive(normalizedSchema);
+ const permissiveSchema = makeSchemaPermissive(schema);
- const validate = validationHelper(headersSchema);
+ const normalizedHeadersSchema = normalizeSchemaKeysToLowercase(headersSchema);
+ const validate = validationHelper(normalizedHeadersSchema);

Why this is correct:

  • getCachedValidator() is a shared utility used by both validateHttpRequestHeaders() (for HTTP headers) and ContextFetcher.validateResponseWithJsonSchema() (for response bodies)
  • HTTP headers are case-insensitive per RFC 7230/9110 and should be normalized
  • Response body JSON properties (e.g., userName, createdAt) are case-sensitive and should NOT be normalized
  • The fix correctly scopes normalization to the headers-only call site

🕐 Pending Recommendations (3)

Items from prior reviews still applicable:


✅ APPROVE

Summary: The delta successfully addresses the critical scoping issue from the prior review. The normalization is now correctly limited to header schemas only, ensuring response body validation remains case-sensitive. The implementation is minimal and precisely targeted. The prior suggestions (nested object test, type guard edge case, unrelated docs change) remain as optional improvements but are not blocking. Ship it! 🚀

Reviewers (2)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-precision 1 0 0 0 0 1 0
Total 1 0 0 0 0 1 0

Note: The precision reviewer identified redundant normalization (schema normalized twice per request), but this was already noted in the prior pullfrog review and is a minor optimization opportunity, not a bug.

@nick-inkeep
Copy link
Copy Markdown
Collaborator Author

Addressing review feedback:

Pullfrog (HIGH — shared getCachedValidator): Already fixed in commit 91ad5b67b. Normalization moved out of getCachedValidator() into validateHttpRequestHeaders() only. Delta review confirmed this is correct.

Claude — nested object test (Minor): Valid suggestion per spec test case #5. In practice, HTTP headers are flat key-value strings (not nested objects), so the recursive path is defensive code. The normalization function handles it correctly — adding a test for completeness would be a nice-to-have but not blocking.

Claude — type guard edge case (Consider): Matches existing makeSchemaPermissive() pattern (line 118) which uses the same type === 'object' guard. Consistency with the existing pattern is the right call — changing one without the other would create divergence.

Claude + Pullfrog — evaluations.mdx (While You're Here): This is auto-generated by Fumadocs during build. Will revert from this PR to keep the diff focused.

Remove auto-generated Fumadocs change that was incidentally bundled
in this PR. Keeps the diff focused on the headersSchema fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) New Issues | Risk: Low | Delta Review

This review covers the 1 commit since the last automated review (91ad5b67b868187e3e).


✅ Delta Assessment

The latest commit (868187e3e chore: revert unrelated evaluations.mdx auto-generated change) addresses the While You're Here item from prior reviews.

What changed:

  • Reverted the incidentally bundled evaluations.mdx auto-generated docs change
  • Keeps the PR diff focused on the headersSchema case sensitivity fix

This completes the feedback loop — all actionable items from prior reviews have been addressed.


📋 Prior Feedback Summary

Issue Status
🔴 Pullfrog HIGH — normalization in shared getCachedValidator() ✅ Fixed in 91ad5b67b
🧹 evaluations.mdx — unrelated docs change ✅ Reverted in 868187e3e (this delta)
🟡 Nested object test — SPEC.md Test Case #5 ℹ️ Author acknowledged; HTTP headers are flat strings, recursive path is defensive code
💭 Type guard edge casetype === 'object' check ℹ️ Author noted consistency with makeSchemaPermissive() pattern

🔍 Final Code Assessment

The implementation is correct and well-scoped:

  1. Root cause fixed: normalizeSchemaKeysToLowercase() correctly lowercases JSON Schema property names and required entries before AJV validation
  2. Scoping correct: Normalization is applied only to headersSchema in validateHttpRequestHeaders(), not in the shared getCachedValidator() which is also used for case-sensitive response body schemas
  3. Test coverage solid: 4 new test cases cover camelCase properties, required entries, mixed case, and filtering behavior
  4. No regression risk: Existing lowercase schemas continue to work identically

✅ APPROVE

Summary: This PR correctly fixes the case sensitivity mismatch between headersSchema property names and lowercased HTTP headers. The implementation is minimal, precisely targeted, and follows HTTP semantics (RFC 7230/9110). All prior review feedback has been addressed. Ship it! 🚀

Reviewers (0)

No new reviewers dispatched for this delta — the only change was a revert of the docs file as previously requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant