Skip to content

fix: emit valid wirefilter for keyed Cloudflare header/cookie conditions - #278

Merged
gfargo merged 1 commit into
mainfrom
fix/cloudflare-header-cookie-keyed-syntax
Aug 24, 2026
Merged

fix: emit valid wirefilter for keyed Cloudflare header/cookie conditions#278
gfargo merged 1 commit into
mainfrom
fix/cloudflare-header-cookie-keyed-syntax

Conversation

@gfargo

@gfargo gfargo commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #269. http.request.headers, http.request.uri.args (query), and http.request.cookies (cookie) all type as Map<Array<String>> per Cloudflare's Ruleset Engine field/function references — indexing one yields Array<String>, so field["key"] eq "value" for a keyed header/cookie condition was an Array-vs-String type mismatch, and field["key"] exists was never valid syntax for a Map-typed field either.

Verification

  • pnpm compile && pnpm jest && pnpm eslint . all clean (1774 tests passing).
  • Mutation-verified: reverted the ExpressionBuilder fix and the WirefilterParser grammar dispatch independently, confirmed the new/updated regression tests fail with legible diffs in both cases, then restored.
  • End-to-end against demos/cloudflare-mock-server.mjs with a real built CLI: synced a rule with keyed header (eq + exists), keyed cookie, and keyed query conditions, fetched the ruleset back from the mock server and confirmed the exact wirefilter sent over the wire:
    (any(http.request.headers["content-type"][*] eq "application/json") and has_key(http.request.headers, "x-api-version") and any(http.request.cookies["session_id"][*] eq "abc123") and any(http.request.uri.args["debug"][*] eq "1"))
    
    Then ran doorman diff again and confirmed zero phantom changes — proving the parser fix round-trips correctly against real (mocked) Cloudflare API responses, not just in isolated unit tests.

Test plan

  • pnpm jest — all 1774 tests pass
  • pnpm compile — clean
  • pnpm eslint . — 0 errors
  • Mutation-verified new regression tests (ExpressionBuilder + WirefilterParser fixes independently)
  • End-to-end verified against cloudflare-mock-server.mjs (sync + diff round-trip)

Closes #269. Verified against Cloudflare's Ruleset Engine field and
function references: http.request.headers, http.request.uri.args
(query), and http.request.cookies (cookie) all type as
Map<Array<String>>. Indexing one yields Array<String>, so the previous
field["key"] eq "value" construct — used for a keyed header or cookie
condition — was an Array-vs-String type mismatch Cloudflare's API
rejects, and field["key"] exists was never valid syntax for a
Map-typed field either.

Fixed by compiling a keyed header/cookie condition to the same
any(field["key"][*] <op> value) / has_key(field, "key") idiom #263
already established for query, via a new shared
ExpressionBuilder.buildKeyedMapExpression helper. Two related fixes
fell out of the same research:

- header now always requires a key and throws a clear error without
  one, since there's no "all headers as one value" fallback the way
  cookie's unkeyed http.cookie is. The key is also lowercased before
  compiling, matching Cloudflare's documented lowercase-keyed header
  map (a mixed-case key would otherwise silently never match).
- cookie now uses http.request.cookies (Cloudflare's actual per-cookie
  Map field) instead of bracket-indexing http.cookie (a scalar String
  with no Map to index at all) when keyed. This field requires
  Cloudflare Pro/Business/Enterprise; doorman emits it regardless of
  plan and lets Cloudflare's API reject it on an unsupported plan, the
  same policy already applied to matches/regex.

WirefilterParser had no grammar for any(...)/has_key(...) function-call
syntax at all, so it could never parse these expressions back into
structured conditions — confirmed this was already silently broken for
the query fix #263 shipped. Verified end-to-end against the Cloudflare
mock server: doorman sync followed by doorman diff now reports zero
phantom changes for a rule with keyed header/cookie/query conditions;
before this fix diff would have reported the rule as unparseable and
re-added it as a "change" every time. Added any(...)/has_key(...)
parsing that reuses the parser's existing comparison/exists AST node
shapes, so leafToCondition/isLeaf/orGroupsToConditions needed no
changes — this closes the round-trip gap for header, cookie, and
(retroactively) the pre-existing query case alike.

Also fixes CloudflareRuleScenarios.test.ts's "header-based conditions"
fixture, which never actually exercised a valid header condition — it
folded the header name into value instead of using key, something only
exposed once fromUnifiedCondition started throwing on a keyed field it
can't build. Updated cloudflare.md's field mapping table and the
operator-mapping section, which still referenced #263's key-ignored bug
as an open gap even though #263 shipped as PR #267 well before this
session started.
@gfargo
gfargo merged commit c529e57 into main Aug 24, 2026
2 checks passed
@gfargo
gfargo deleted the fix/cloudflare-header-cookie-keyed-syntax branch August 24, 2026 20:52
github-actions Bot pushed a commit that referenced this pull request Aug 24, 2026
## [3.15.2](v3.15.1...v3.15.2) (2026-08-24)

### Bug Fixes

* emit valid wirefilter for keyed Cloudflare header/cookie conditions ([#278](#278)) ([c529e57](c529e57)), closes [#269](#269) [#263](#263) [#263](#263) [#263](#263) [#263](#263) [#267](#267)
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 3.15.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare keyed header/cookie conditions likely emit wirefilter the API rejects (Array-vs-String, and indexing a String field)

1 participant