Skip to content

feat: RFC compliant typed headers#360

Open
nielsenko wants to merge 75 commits into
serverpod:mainfrom
nielsenko:feat-rfc-compliant-headers
Open

feat: RFC compliant typed headers#360
nielsenko wants to merge 75 commits into
serverpod:mainfrom
nielsenko:feat-rfc-compliant-headers

Conversation

@nielsenko

@nielsenko nielsenko commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Description

Overhaul typed headers to be fully (/more?) RFC compliant. Parsing is still lenient (except for security related headers), but construction (and serialization) is always strict.

Related Issues

Pre-Launch Checklist

Please ensure that your PR meets the following requirements before submitting:

  • This update focuses on a single feature or bug fix. (For multiple fixes, please submit separate PRs.)
  • I have read and followed the Dart Style Guide and formatted the code using dart format.
  • I have referenced at least one issue this PR fixes or is related to.
  • I have updated/added relevant documentation (doc comments with ///), ensuring consistency with existing project documentation.
  • I have added new tests to verify the changes.
  • All existing and new tests pass successfully.
  • I have documented any breaking changes below.

Breaking Changes

  • Includes breaking changes.
  • No breaking changes.

Many typed headers changed, both their shape (Dart interface) and semantics (behavior or parse, etc.).

Additional Notes

These changes are landing now in concert with serverpod 4.0

Summary by CodeRabbit

  • New Features

    • Exported new HTTP header type primitives: Delta Seconds, ETag Value, Header Scanner, Host, Language Tag, Origin, Parameter Value, Token, and Token Value for advanced header validation use cases.
    • Added support for multiple range units in Accept-Ranges header parsing.
    • Added optional report-to parameter support in Cross-Origin policy headers.
  • Bug Fixes & Improvements

    • Improved HTTP header parsing robustness: malformed quality values now default to 1.0 instead of causing request rejection.
    • Enhanced case-insensitive header field name handling across Accept-Control and Vary headers.
    • Strengthened RFC 9110 compliance for Transfer-Encoding, Set-Cookie, and Origin validation.
    • Fixed weak ETag handling in If-Range requests to prevent improper range request satisfaction.

@nielsenko nielsenko self-assigned this Jun 12, 2026
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 884a2290-2df5-4fb8-8fc3-d15cb92f2006

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR raises the Dart SDK baseline to 3.10.3, updates CI gating to match, and refactors multiple typed HTTP header parsers and primitives to use stricter canonicalization, new parsing helpers, and revised validation semantics. Tests and IO behavior were updated to match the new header contracts.

Changes

CI and SDK baseline

Layer / File(s) Summary
Workflow and SDK version
.github/workflows/ci.yaml, pubspec.yaml
The Dart SDK floor and CI job matrices now target 3.10.3.

Typed headers and primitives

Layer / File(s) Summary
New header primitives
packages/relic_core/lib/relic_core.dart, packages/relic_core/lib/src/headers/typed/primitives/*
New primitives for tokens, hosts, language tags, origins, q-values, delta seconds, ETags, parameter values, and header scanning are added and exported.
Typed header parsing and encoding
packages/relic_core/lib/src/headers/typed/headers/*, packages/relic_core/lib/src/headers/typed/headers/util/*
Typed header implementations now use the new primitives for canonicalization, structured parsing, and encoding across cache, content, CORS, auth, and related headers.
IO range handling
packages/relic_io/lib/src/adapter/http_response_extension.dart, packages/relic_io/lib/src/io/static/static_handler.dart
The IO adapter and static file handler now consume the updated typed headers for transfer encoding and If-Range validation.
Typed header tests
packages/relic_core/test/headers/header_test.dart, packages/relic_core/test/headers/typed/*, packages/relic_core/test/message/apply_headers_test.dart, packages/relic_io/test/static/if_range_test.dart
The typed header test suites are updated to match the new parsing, encoding, canonicalization, and validation behavior.

Sequence Diagram(s)

sequenceDiagram
  participant PackageExports as relic_core.dart
  participant Parsers as typed header parsers
  participant IO as relic_io handlers
  participant Tests as typed tests
  PackageExports->>Parsers: export new primitives
  Parsers->>IO: provide parsed headers and validators
  Parsers->>Tests: expose new parse/encode behavior
  IO->>Tests: verified by range and transfer-encoding tests
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related issues

Possibly related PRs

  • serverpod/relic#159: Shares the same q-value parsing/encoding and typed header parser refactoring area in relic_core.
  • serverpod/relic#142: Directly overlaps with the HostHeader/host parsing work and the move away from Uri for host semantics.
  • serverpod/relic#318: Also changes CI coverage gating and workflow conditions in the same repository.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@nielsenko nielsenko force-pushed the feat-rfc-compliant-headers branch 3 times, most recently from da8791d to 9608c86 Compare June 12, 2026 08:39
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.17935% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.72%. Comparing base (0112560) to head (843d531).

Files with missing lines Patch % Lines
...lib/src/headers/typed/primitives/language_tag.dart 84.28% 22 Missing ⚠️
...b/src/headers/typed/primitives/header_scanner.dart 91.42% 9 Missing ⚠️
...ic_core/lib/src/headers/typed/primitives/host.dart 94.48% 7 Missing ⚠️
...b/src/headers/typed/headers/set_cookie_header.dart 86.84% 5 Missing ⚠️
...ore/lib/src/headers/typed/headers/host_header.dart 81.81% 4 Missing ⚠️
...e/lib/src/headers/typed/primitives/etag_value.dart 86.66% 4 Missing ⚠️
...ore/lib/src/headers/typed/headers/util/qvalue.dart 70.00% 3 Missing ⚠️
.../lib/src/headers/typed/headers/util/report_to.dart 84.21% 3 Missing ⚠️
.../src/headers/typed/primitives/parameter_value.dart 89.28% 3 Missing ⚠️
.../headers/typed/headers/accept_encoding_header.dart 80.00% 2 Missing ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #360      +/-   ##
==========================================
+ Coverage   91.21%   91.72%   +0.51%     
==========================================
  Files          98      108      +10     
  Lines        3849     4510     +661     
  Branches     1964     2291     +327     
==========================================
+ Hits         3511     4137     +626     
- Misses        338      373      +35     
Flag Coverage Δ
relic_core 91.54% <92.13%> (+0.61%) ⬆️
relic_io 93.34% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

nielsenko added 20 commits June 12, 2026 10:48
@nielsenko nielsenko force-pushed the feat-rfc-compliant-headers branch 3 times, most recently from 34062ed to 1fe6945 Compare June 12, 2026 09:28
@nielsenko nielsenko changed the title Feat rfc compliant headers feat: RFC compliant typed headers Jun 12, 2026
@nielsenko

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/relic_core/lib/src/headers/typed/headers/accept_header.dart (1)

79-114: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve media-range parameters instead of discarding them.

MediaRange.parse() now walks the ; parameters only to extract q, and _encode() serializes just $type/$subtype plus the weight. That means inputs like text/html;level=1 or application/json;charset=utf-8;q=0.8 round-trip without their media parameters, which changes content-negotiation semantics rather than just normalizing syntax. This needs a parameter model on MediaRange and matching encode logic before the Accept parser can be considered RFC-compliant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relic_core/lib/src/headers/typed/headers/accept_header.dart` around
lines 79 - 114, MediaRange.parse currently only extracts the q-value and drops
all other media parameters; update the MediaRange model to include a parameters
field (e.g., Map<String,String> or List<KeyValuePair>) and modify
MediaRange.parse to parse every semicolon parameter into that field
(case-preserving keys but compare keys case-insensitively for 'q' to set
quality), and update the MediaRange constructor to accept parameters; then
change _encode to serialize type/subtype followed by all preserved parameters in
their original order and finally append the ;q=... token if a quality is present
(ensuring formatting via formatQValue) so inputs like "text/html;level=1" or
"application/json;charset=utf-8;q=0.8" round-trip correctly.
packages/relic_core/lib/src/headers/typed/headers/content_encoding_header.dart (1)

36-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize first, then de-duplicate, in content_encoding_header.dart, te_header.dart, and vary_header.dart. Each of these paths removes duplicates on the raw token first and only lowercases afterward, so case-only variants like gzip, GZIP or Accept-Encoding, accept-encoding survive as duplicate canonical entries. The shared fix is to apply case normalization before uniqueness filtering, or to perform a second dedupe pass on the canonicalized value before storing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/relic_core/lib/src/headers/typed/headers/content_encoding_header.dart`
around lines 36 - 40, The current flow maps splitValues to ContentEncoding.parse
and then deduplicates raw tokens, which preserves case-only duplicates; update
the logic in content_encoding_header.dart (and mirror in te_header.dart and
vary_header.dart) to normalize/canonicalize each token (e.g., lowercase or use
ContentEncoding.parse()/canonical form) before performing uniqueness filtering,
or perform a second dedupe pass after mapping to the canonical value;
specifically, ensure the code that builds parsedEncodings (the splitValues.map
-> ContentEncoding.parse pipeline) lowercases/canonicalizes values first or
removes duplicates from the parsedEncodings list before calling
ContentEncodingHeader.encodings so case-only variants like "gzip" and "GZIP" are
collapsed into one entry.
packages/relic_core/lib/src/headers/typed/headers/set_cookie_header.dart (1)

245-263: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the unresolved dartdoc member references.

The pipeline is already warning that [Cookie.sameSite] and [Cookie.secure] do not resolve here. Those links should point at SetCookieHeader, not Cookie.

Suggested fix
 /// Cookie cross-site availability configuration.
 ///
-/// The value of [Cookie.sameSite], which defines whether an
+/// The value of [SetCookieHeader.sameSite], which defines whether a
 /// HTTP cookie is available from other sites or not.
 ///
 /// Has three possible values: [lax], [strict] and [none].
 final class SameSite {
@@
   /// Cookie with this value will be sent in all requests.
   ///
-  /// [Cookie.secure] must also be set to true, otherwise the `none` value
+  /// [SetCookieHeader.secure] must also be set to true, otherwise the `none` value
   /// will have no effect.
   static const none = SameSite._('None');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relic_core/lib/src/headers/typed/headers/set_cookie_header.dart`
around lines 245 - 263, The documentation on the SameSite class contains
unresolved references to Cookie.sameSite and Cookie.secure; update those dartdoc
links to point at the SetCookieHeader API instead (e.g., replace
`[Cookie.sameSite]` with `[SetCookieHeader.sameSite]` and `[Cookie.secure]` with
`[SetCookieHeader.secure]`) so the links resolve correctly; modify the doc
comments in the SameSite class accordingly.

Source: Pipeline failures

packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart (1)

309-328: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate and strip the Digest scheme before scanning auth-params.

DigestAuthorizationHeader.parse() currently regex-scans the entire input, so strings like Basic username="u", realm="r", nonce="n", uri="/", response="x" or even bare username="u", ... are accepted as Digest headers. That makes the direct parser inconsistent with BasicAuthorizationHeader.parse() / BearerAuthorizationHeader.parse() and weakens scheme validation.

Suggested fix
   factory DigestAuthorizationHeader.parse(final String value) {
     if (value.isEmpty) {
       throw const FormatException('Digest token cannot be empty.');
     }
+    final paramsValue = _stripScheme(value, prefix);

     // Each auth-param is `token = ( token / quoted-string )` (RFC 7616 3.4):
     // quoted-string values are DQUOTE-wrapped (group 2, with quoted-pair
     // escapes), token values are bare (group 3). Accepting both is required
     // because conformant peers send algorithm/qop/nc/stale unquoted.
     final Map<String, String> params = {};
     final regex = RegExp(r'(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^",\s]+))');
-    for (final match in regex.allMatches(value)) {
+    for (final match in regex.allMatches(paramsValue)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart`
around lines 309 - 328, DigestAuthorizationHeader.parse currently scans the
whole input and accepts non-Digest schemes; update parse to validate and strip
the "Digest" scheme token (case-insensitive) before running the auth-param
regex: check the input begins with "Digest" followed by whitespace, remove that
scheme prefix and trim the remainder, throw a FormatException if the scheme is
missing/incorrect or the remainder is empty, then proceed to extract params with
the existing regex and keep using _unescapeQuoted and Token.validate for values.
packages/relic_core/lib/src/headers/typed/headers/from_header.dart (1)

24-34: ⚠️ Potential issue | 🟠 Major

Fix FromHeader.parse “kept as-is” claim: splitTrimAndFilterUnique is not quote-aware

  • FromHeader.parse routes values through splitTrimAndFilterUnique(), which splits using String.split(',') (not quote-aware), so "Doe, John" <john@example.com> will be split/corrupted before reaching FromHeader.emails(...).
  • Update the comment/behavior (use a quote-aware mailbox-list splitter or avoid comma-splitting) so the stated “kept as-is” meaning matches actual parsing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relic_core/lib/src/headers/typed/headers/from_header.dart` around
lines 24 - 34, FromHeader.parse currently calls splitTrimAndFilterUnique (which
uses naive String.split(',') and is not quote-aware), causing quoted
display-names like "Doe, John" <john@example.com> to be incorrectly split;
update FromHeader.parse to stop using splitTrimAndFilterUnique and either pass
the original Iterable<String> values through unchanged to FromHeader.emails or
replace the splitter with a quote-aware mailbox-list splitter that follows RFC
5322 semantics; locate the call in FromHeader.parse and change it to use a
proper quote-aware splitter (or no splitting) so the earlier comment “kept as-is
rather than format-validated” is accurate.
🧹 Nitpick comments (1)
packages/relic/test/headers/header_test.dart (1)

856-859: ⚡ Quick win

Add a versioned UpgradeProtocol case to this round-trip matrix.

The breaking change here is that version is now an opaque String, but this shared coverage still only exercises a protocol without any version. Using something like UpgradeProtocol(protocol: 'HTTP', version: '2.0') would actually verify the new parse/encode/equality contract and catch the formatting-preservation regressions this PR is trying to prevent. This follows the updated UpgradeProtocol semantics described in the PR context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/relic/test/headers/header_test.dart` around lines 856 - 859, The
round-trip matrix for Headers.upgrade currently only tests UpgradeProtocol
without a version; update the test to include a versioned case so
encoding/decoding and equality are exercised with the new opaque String version
semantics. Modify the matrix entry that sets h.upgrade =
UpgradeHeader.protocols([...]) to include an additional UpgradeProtocol instance
with both protocol and version set (e.g., UpgradeProtocol(protocol: 'HTTP',
version: '2.0')) alongside the existing protocol-only case so Headers.upgrade,
UpgradeHeader, and UpgradeProtocol parsing/encoding are validated for the
versioned format.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/relic_core/lib/src/headers/typed/headers/accept_ranges_header.dart`:
- Around line 28-49: In AcceptRangesHeader.parse, detect if the parsed,
lower-cased units list contains 'none' together with any other unit and reject
that case by throwing a FormatException (e.g., "Invalid value: 'none' must be
alone"); update the factory AcceptRangesHeader.parse to perform this exclusivity
check after mapping Token.validate and before returning
AcceptRangesHeader._(List.unmodifiable(units)), so the typed model never
contains contradictory states for isNone/isBytes.

In `@packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart`:
- Around line 298-302: The constructor currently validates algorithm/qop/nc
using Token.validate, but nc must be RFC-compliant (exactly 8 hex digits) per
Digest specs; replace the Token.validate(nc!) check with an RFC nc validator:
verify nc matches /^[0-9A-Fa-f]{8}$/ (or call a new helper e.g.
NCAuthorization.validateNc) and throw the same validation error if it fails,
keeping Token.validate for algorithm and qop and referencing the nc variable and
Token.validate call sites to locate the change.
- Around line 465-469: The _quoteString function currently only escapes quotes
and backslashes but allows raw control characters (e.g., CR/LF) which can enable
header injection; modify _quoteString to validate the input string for ASCII
control characters (U+0000–U+001F and U+007F) and reject them before quoting by
throwing a descriptive exception (e.g., FormatException or ArgumentError) when
any control character is present; keep the existing escapes for '"' and '\' and
only proceed to return the quoted string if validation passes so Digest/header
values cannot contain raw control characters.

In `@packages/relic_core/lib/src/headers/typed/headers/cache_control_header.dart`:
- Around line 125-134: The _delta function collapses a bare directive (e.g.,
"max-stale") into null making it indistinguishable from an absent directive;
change the parsing to preserve the bare form by returning a sentinel value
(e.g., -1) or converting the return type to a tri-state (e.g., enum or int? with
distinct values) so callers can detect "present-without-value" vs "absent".
Update _delta (and its callers that handle the Cache-Control max-stale handling
and the fields referenced around lines 209-210) to treat the sentinel/tri-state
as "bare directive present" when encoding and keep null only for truly absent
directives. Ensure re-encoding emits "max-stale" when the sentinel/tri-state
indicates presence-without-value.
- Around line 176-178: The code currently throws when a directive name is not in
_validDirectiveSet; update the parser in cache_control_header.dart (the block
that checks if (!_validDirectiveSet.contains(name))) to ignore unknown/extension
directives instead of throwing — simply skip unrecognized names (and their
optional values) and continue parsing so forward-compatible Cache-Control
extension directives are accepted; do not remove handling for known directives,
just replace the throw const FormatException('Invalid directive') with logic to
consume/skip the unknown directive and proceed.

In
`@packages/relic_core/lib/src/headers/typed/headers/content_language_header.dart`:
- Around line 32-41: The code currently validates tags with
LanguageTag.parse(language) but returns the original raw string, which preserves
case and makes comparisons/hashCode case-sensitive; change the logic in the
block that builds languages so it stores a canonical/normalized form from the
parsed tag (use the parsed LanguageTag instance’s canonical representation,
e.g., parsedTag.toString() or its canonicalization API) instead of the raw
input, and ensure the same normalized values are used by the header's equality
(==) and hashCode calculations so `en-US` and `EN-us` compare equal.

In `@packages/relic_core/lib/src/headers/typed/headers/content_range_header.dart`:
- Around line 31-44: The ContentRangeHeader constructor currently allows
negative start, end, or size values; update the ContentRangeHeader constructor
to validate that any non-null numeric parameters (start, end, size) are >= 0 and
throw a FormatException with an appropriate message if any are negative. Locate
the public ContentRangeHeader constructor and add these checks (in addition to
the existing null/coherence and start<=end checks) so direct construction cannot
produce negative-range values that would serialize invalid Content-Range
headers.

In
`@packages/relic_core/lib/src/headers/typed/headers/permission_policy_header.dart`:
- Around line 29-40: The constructor PermissionsPolicyHeader.directives
currently only rejects empty names; update it to validate each
PermissionsPolicyDirective.name as a structured-field token (no spaces or
control chars, only allowed token characters) and throw a FormatException for
any invalid token. Specifically, in PermissionsPolicyHeader.directives iterate
the directives and check directive.name against a structured-field token pattern
(e.g., token characters per RFC — no whitespace or embedded quotes/special
separators); if any name fails, throw a clear FormatException (e.g.,
"Permissions-Policy feature name is invalid") so malformed names like "   " or
"geolocation allow" are rejected at construction. Ensure you reference
PermissionsPolicyHeader.directives and PermissionsPolicyDirective.name when
adding the validation.

In `@packages/relic_core/lib/src/headers/typed/headers/set_cookie_header.dart`:
- Around line 124-160: The switch in set_cookie_header.dart inside the attribute
parsing block (switch on attrName.toLowerCase()) is missing terminators causing
fall-through; update each case (cases 'samesite', 'path', 'domain', 'max-age',
'expires', 'secure', 'httponly') to end with an explicit break (or return) after
setting the corresponding variable or throwing, so execution does not continue
into subsequent cases; ensure the default remains as-is.

In `@packages/relic_core/lib/src/headers/typed/headers/util/cookie_util.dart`:
- Around line 95-103: The validateCookiePath function currently allows non-ASCII
characters; update its validation loop (in validateCookiePath) to also reject
any code units >= 0x80 by throwing the same FormatException so path bytes
outside the 0x00-0x7F ASCII range are treated as invalid; keep the existing
checks for CTLs, DEL and ';' and return the path only if all bytes are within
allowed ASCII range.

In `@packages/relic_core/lib/src/headers/typed/headers/util/report_to.dart`:
- Around line 35-37: The function encodeReportToParam currently only escapes
backslash and quote but must first reject control characters (e.g. CR, LF and
other C0 controls) to avoid header-splitting; update encodeReportToParam to scan
value for any code unit <= 0x1F or == 0x7F and throw an
ArgumentError/FormatException if any are found, then proceed to escape '\' and
'"' and return 'report-to="...'" as before (refer to encodeReportToParam for the
exact location to add the validation).

In `@packages/relic_core/lib/src/headers/typed/primitives/host.dart`:
- Around line 183-184: The single-line if containing the ALPHA check (the if
with condition "(c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A)" that
returns true) must be expanded to use braces: replace the single-line form with
a block using { return true; } so the statement is not a single-line if; update
the same function containing that if (the method with the ALPHA check) to use
the braced form to silence the analyzer warning.
- Around line 42-56: The host validation currently only rejects a small
forbidden set; update the host validation (around the loop using
_isForbiddenHostChar and the subsequent IP-literal check) to also enforce RFC
3986 reg-name rules: if the host is not an IP-literal (_validateIpLiteral) and
not an IPv4 address, ensure the host bytes are ASCII and every character is
either an unreserved character, a sub-delims character, or a valid
percent-encoding sequence ("%HEXHEX"); reject and throw FormatException for any
non-ASCII byte or invalid percent-encoding (refer to the host variable,
_isForbiddenHostChar, _validateIpLiteral and encode logic when implementing
this).

In `@packages/relic_core/lib/src/headers/typed/primitives/language_tag.dart`:
- Around line 104-129: The parser currently allows duplicate variant subtags and
duplicate extension singletons; update the parsing in the variant loop and the
extension loop (the code using _isVariant, _isExtensionSingleton, canonical,
raw, and source) to track seen items and reject repeats: create a local
Set<String> seenVariants and check before adding each _lower(raw[i]) in the
variant loop (throw FormatException with source on duplicate), and create a
Set<String> seenExtensionSingletons for the extension loop to check the
singleton (and/or its lowercased form) before accepting it (throw
FormatException if the singleton already seen); keep canonical additions and
existing subtag validation but ensure duplicates cause failure.

In
`@packages/relic_core/test/headers/typed/access_control_allow_origin_header_test.dart`:
- Around line 14-15: Remove the brittle hashCode inequality assertion comparing
wildcard.hashCode and opaque.hashCode and keep the semantic equality check only;
update the test in access_control_allow_origin_header_test.dart to delete the
line referencing wildcard.hashCode != opaque.hashCode and rely on
expect(wildcard == opaque, isFalse) (or expect(wildcard != opaque, isTrue)) to
assert inequality between the wildcard and opaque instances.

In `@packages/relic_core/test/headers/typed/transfer_encoding_header_test.dart`:
- Around line 78-84: The test currently asserts that
TransferEncodingHeader.parse(['custom-encoding']) throws, which incorrectly
rejects RFC‑allowed extension codings; update the test to accept extension
tokens instead of expecting a FormatException — modify the assertion in the test
that calls TransferEncodingHeader.parse to expect successful parsing (or a
specific representation for extension codings) rather than
throwsFormatException, and ensure any canonicalization code paths in
TransferEncodingHeader.parse/TransferCoding handling extension tokens (e.g.,
parsing logic used by TransferEncodingHeader.parse and the TransferCoding
representation) are exercised by this test.

In
`@packages/relic/test/headers/typed/access_control_allow_origin_header_test.dart`:
- Around line 73-83: The test currently only checks for any BadRequestException
from getServerRequestHeaders; update it to assert the specific rejection reason
for path-containing origins by matching the exception message or a specific
error property on BadRequestException (e.g., check e.message contains "origins
must not include paths" or "trailing slash" or the server's exact error text).
Replace the generic throwsA(isA<BadRequestException>()) with
throwsA(predicate((e) => e is BadRequestException && e.message.contains('<server
path-rejection message>'))) so the test validates the Fetch-style "origins must
not include paths" behavior for accessControlAllowOrigin.

---

Outside diff comments:
In `@packages/relic_core/lib/src/headers/typed/headers/accept_header.dart`:
- Around line 79-114: MediaRange.parse currently only extracts the q-value and
drops all other media parameters; update the MediaRange model to include a
parameters field (e.g., Map<String,String> or List<KeyValuePair>) and modify
MediaRange.parse to parse every semicolon parameter into that field
(case-preserving keys but compare keys case-insensitively for 'q' to set
quality), and update the MediaRange constructor to accept parameters; then
change _encode to serialize type/subtype followed by all preserved parameters in
their original order and finally append the ;q=... token if a quality is present
(ensuring formatting via formatQValue) so inputs like "text/html;level=1" or
"application/json;charset=utf-8;q=0.8" round-trip correctly.

In `@packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart`:
- Around line 309-328: DigestAuthorizationHeader.parse currently scans the whole
input and accepts non-Digest schemes; update parse to validate and strip the
"Digest" scheme token (case-insensitive) before running the auth-param regex:
check the input begins with "Digest" followed by whitespace, remove that scheme
prefix and trim the remainder, throw a FormatException if the scheme is
missing/incorrect or the remainder is empty, then proceed to extract params with
the existing regex and keep using _unescapeQuoted and Token.validate for values.

In
`@packages/relic_core/lib/src/headers/typed/headers/content_encoding_header.dart`:
- Around line 36-40: The current flow maps splitValues to ContentEncoding.parse
and then deduplicates raw tokens, which preserves case-only duplicates; update
the logic in content_encoding_header.dart (and mirror in te_header.dart and
vary_header.dart) to normalize/canonicalize each token (e.g., lowercase or use
ContentEncoding.parse()/canonical form) before performing uniqueness filtering,
or perform a second dedupe pass after mapping to the canonical value;
specifically, ensure the code that builds parsedEncodings (the splitValues.map
-> ContentEncoding.parse pipeline) lowercases/canonicalizes values first or
removes duplicates from the parsedEncodings list before calling
ContentEncodingHeader.encodings so case-only variants like "gzip" and "GZIP" are
collapsed into one entry.

In `@packages/relic_core/lib/src/headers/typed/headers/from_header.dart`:
- Around line 24-34: FromHeader.parse currently calls splitTrimAndFilterUnique
(which uses naive String.split(',') and is not quote-aware), causing quoted
display-names like "Doe, John" <john@example.com> to be incorrectly split;
update FromHeader.parse to stop using splitTrimAndFilterUnique and either pass
the original Iterable<String> values through unchanged to FromHeader.emails or
replace the splitter with a quote-aware mailbox-list splitter that follows RFC
5322 semantics; locate the call in FromHeader.parse and change it to use a
proper quote-aware splitter (or no splitting) so the earlier comment “kept as-is
rather than format-validated” is accurate.

In `@packages/relic_core/lib/src/headers/typed/headers/set_cookie_header.dart`:
- Around line 245-263: The documentation on the SameSite class contains
unresolved references to Cookie.sameSite and Cookie.secure; update those dartdoc
links to point at the SetCookieHeader API instead (e.g., replace
`[Cookie.sameSite]` with `[SetCookieHeader.sameSite]` and `[Cookie.secure]` with
`[SetCookieHeader.secure]`) so the links resolve correctly; modify the doc
comments in the SameSite class accordingly.

---

Nitpick comments:
In `@packages/relic/test/headers/header_test.dart`:
- Around line 856-859: The round-trip matrix for Headers.upgrade currently only
tests UpgradeProtocol without a version; update the test to include a versioned
case so encoding/decoding and equality are exercised with the new opaque String
version semantics. Modify the matrix entry that sets h.upgrade =
UpgradeHeader.protocols([...]) to include an additional UpgradeProtocol instance
with both protocol and version set (e.g., UpgradeProtocol(protocol: 'HTTP',
version: '2.0')) alongside the existing protocol-only case so Headers.upgrade,
UpgradeHeader, and UpgradeProtocol parsing/encoding are validated for the
versioned format.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4e5e23f4-1764-45d0-96bc-88e9c51ae11e

📥 Commits

Reviewing files that changed from the base of the PR and between 0112560 and c768b55.

📒 Files selected for processing (104)
  • .github/workflows/ci.yaml
  • packages/relic/test/headers/header_test.dart
  • packages/relic/test/headers/typed/accept_encoding_header_test.dart
  • packages/relic/test/headers/typed/accept_header_test.dart
  • packages/relic/test/headers/typed/accept_language_test.dart
  • packages/relic/test/headers/typed/accept_ranges_header_test.dart
  • packages/relic/test/headers/typed/access_control_allow_headers_header_test.dart
  • packages/relic/test/headers/typed/access_control_allow_origin_header_test.dart
  • packages/relic/test/headers/typed/access_control_expose_headers_header_test.dart
  • packages/relic/test/headers/typed/authorization_header_test.dart
  • packages/relic/test/headers/typed/cache_control_header_test.dart
  • packages/relic/test/headers/typed/connection_header_test.dart
  • packages/relic/test/headers/typed/content_encoding_header_test.dart
  • packages/relic/test/headers/typed/content_language_header_test.dart
  • packages/relic/test/headers/typed/content_security_policy_header_test.dart
  • packages/relic/test/headers/typed/cookie_header_test.dart
  • packages/relic/test/headers/typed/expect_header_test.dart
  • packages/relic/test/headers/typed/from_header_test.dart
  • packages/relic/test/headers/typed/if_range_header_test.dart
  • packages/relic/test/headers/typed/permissions_policy_header_test.dart
  • packages/relic/test/headers/typed/proxy_authorization_header_test.dart
  • packages/relic/test/headers/typed/referrer_policy_header_test.dart
  • packages/relic/test/headers/typed/retry_after_header_test.dart
  • packages/relic/test/headers/typed/sec_fetch_dest_header_test.dart
  • packages/relic/test/headers/typed/sec_fetch_mode_header_test.dart
  • packages/relic/test/headers/typed/sec_fetch_site_header_test.dart
  • packages/relic/test/headers/typed/set_cookie_header_test.dart
  • packages/relic/test/headers/typed/te_header_test.dart
  • packages/relic/test/headers/typed/transfer_encoding_header_test.dart
  • packages/relic/test/headers/typed/upgrade_header_test.dart
  • packages/relic/test/headers/typed/vary_header_test.dart
  • packages/relic/test/message/apply_headers_test.dart
  • packages/relic_core/lib/relic_core.dart
  • packages/relic_core/lib/src/headers/typed/headers/accept_encoding_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/accept_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/accept_language_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/accept_ranges_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/access_control_allow_headers_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/access_control_allow_origin_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/access_control_expose_headers_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/cache_control_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/clear_site_data_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/connection_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/content_encoding_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/content_language_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/content_range_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/content_security_policy_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/cookie_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/cross_origin_embedder_policy_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/cross_origin_opener_policy_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/expect_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/from_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/host_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/if_range_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/permission_policy_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/range_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/referrer_policy_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/retry_after_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/sec_fetch_dest_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/sec_fetch_mode_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/sec_fetch_site_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/set_cookie_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/strict_transport_security_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/te_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/transfer_encoding_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/upgrade_header.dart
  • packages/relic_core/lib/src/headers/typed/headers/util/cookie_util.dart
  • packages/relic_core/lib/src/headers/typed/headers/util/qvalue.dart
  • packages/relic_core/lib/src/headers/typed/headers/util/report_to.dart
  • packages/relic_core/lib/src/headers/typed/headers/vary_header.dart
  • packages/relic_core/lib/src/headers/typed/primitives/delta_seconds.dart
  • packages/relic_core/lib/src/headers/typed/primitives/etag_value.dart
  • packages/relic_core/lib/src/headers/typed/primitives/header_scanner.dart
  • packages/relic_core/lib/src/headers/typed/primitives/host.dart
  • packages/relic_core/lib/src/headers/typed/primitives/language_tag.dart
  • packages/relic_core/lib/src/headers/typed/primitives/origin.dart
  • packages/relic_core/lib/src/headers/typed/primitives/parameter_value.dart
  • packages/relic_core/lib/src/headers/typed/primitives/token.dart
  • packages/relic_core/test/headers/typed/access_control_allow_origin_header_test.dart
  • packages/relic_core/test/headers/typed/authorization_header_test.dart
  • packages/relic_core/test/headers/typed/connection_header_test.dart
  • packages/relic_core/test/headers/typed/content_range_header_test.dart
  • packages/relic_core/test/headers/typed/cross_origin_policy_header_test.dart
  • packages/relic_core/test/headers/typed/expect_header_test.dart
  • packages/relic_core/test/headers/typed/host_header_test.dart
  • packages/relic_core/test/headers/typed/permission_policy_header_test.dart
  • packages/relic_core/test/headers/typed/primitives/delta_seconds_test.dart
  • packages/relic_core/test/headers/typed/primitives/etag_value_test.dart
  • packages/relic_core/test/headers/typed/primitives/header_scanner_test.dart
  • packages/relic_core/test/headers/typed/primitives/host_test.dart
  • packages/relic_core/test/headers/typed/primitives/language_tag_test.dart
  • packages/relic_core/test/headers/typed/primitives/origin_test.dart
  • packages/relic_core/test/headers/typed/primitives/parameter_value_test.dart
  • packages/relic_core/test/headers/typed/primitives/token_test.dart
  • packages/relic_core/test/headers/typed/range_header_test.dart
  • packages/relic_core/test/headers/typed/set_cookie_header_test.dart
  • packages/relic_core/test/headers/typed/strict_transport_security_header_test.dart
  • packages/relic_core/test/headers/typed/transfer_encoding_header_test.dart
  • packages/relic_core/test/headers/typed/util/qvalue_test.dart
  • packages/relic_io/lib/src/adapter/http_response_extension.dart
  • packages/relic_io/lib/src/io/static/static_handler.dart
  • packages/relic_io/test/static/if_range_test.dart
  • pubspec.yaml

Comment thread packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart Outdated
Comment thread packages/relic_core/lib/src/headers/typed/headers/authorization_header.dart Outdated
Comment thread packages/relic_core/lib/src/headers/typed/primitives/host.dart Outdated
@nielsenko nielsenko force-pushed the feat-rfc-compliant-headers branch 2 times, most recently from a008fa0 to 2137809 Compare June 12, 2026 10:18
@nielsenko nielsenko force-pushed the feat-rfc-compliant-headers branch from 2137809 to 55aee1d Compare June 12, 2026 10:55
@nielsenko nielsenko requested a review from a team June 12, 2026 12:23

@marcelomendoncasoares marcelomendoncasoares left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving in behalf of all the agents that have already done it.

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.

bug: SetCookieHeader emits invalid Domain (and Path) attributes via Uri.toString() Overhaul typed header parsers

2 participants