Skip to content

fix(headers): stop rewriting raw header values - #2310

Merged
glennawatson merged 1 commit into
mainfrom
fix/raw-header-value-preservation
Aug 18, 2026
Merged

fix(headers): stop rewriting raw header values#2310
glennawatson merged 1 commit into
mainfrom
fix/raw-header-value-preservation

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix.

What is the new behavior?

A header value supplied verbatim reaches the wire byte for byte, however many headers the method declares.

  • The presence check no longer enumerates the header collection. It goes through HttpHeaders.NonValidated, which reports key presence without parsing or materializing any stored value, and never throws for a name the collection does not accept.
  • Both request paths are fixed by one change. The check moved into HttpHeaderApplier, which the reflection request builder and the source-generated request runner already share, so the two copies cannot drift apart again.
  • On net4x, where NonValidated does not exist, the check probes the single named header. That leaves every other stored value untouched. Contains throws when the name belongs to the other collection or is not a legal token; neither name can be present, so both are reported as absent.

What is the current behavior?

A raw header value is silently reformatted once a method declares more than one header.

  • Enumerating HttpHeaders forces lazy parsing of every value already stored and writes the parsed form back, so applying header N rewrote headers 1..N-1.

  • RefitSettings.ValidateHeaders defaults to false, so values go through TryAddWithoutValidation and are expected to reach the wire unchanged. Instead:

    declared    -> Accept: application/vnd.api.json;version=3.4.1
    one header  -> Accept: application/vnd.api.json;version=3.4.1
    two headers -> Accept: application/vnd.api.json; version=3.4.1
    

    Both forms are equivalent under RFC 9110, but servers matching the media type as an exact string reject the rewritten value.

  • Refit.Reflection was affected on every target framework. The generated path was affected on net462 through net481 only, since net6.0+ already used NonValidated there.

Closes #2306

What might this PR break?

None.

  • Callers that relied on the reformatted value were relying on a value Refit was never asked to produce.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

The new tests fail against the previous implementation and pass against this one. ReflectionRawHeaderValueTests reproduces the report at the interface level: the one-header method passes either way, the two-header method only passes with the fix. RawHeaderValueScenarios drives the same assertions through both paths' SetHeader seam, the way the existing HeaderValidationScenarios does.

Assertions read Headers.NonValidated, so they see what is actually stored rather than the parsed projection GetValues would hand back.

- Check header presence through NonValidated instead of enumerating the
  collection. Enumerating forces the framework to parse every value already
  stored and write the parsed form back, so a value added verbatim was
  rewritten as soon as a second header was applied.
- A method declaring "Accept: application/vnd.api.json;version=3.4.1"
  alongside any other header sent it with a space after the semicolon.
  Servers matching the media type as an exact string rejected it.
- Fix both request paths at once by moving the check into HttpHeaderApplier,
  which the reflection builder and the generated runner already share. The
  reflection builder was affected on every target framework; the generated
  runner only on net4x.
- On net4x, where NonValidated does not exist, probe the single named header
  instead. It leaves every other stored value untouched.

Fixes #2306
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.91%. Comparing base (74cbb64) to head (f5ca782).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2310      +/-   ##
==========================================
- Coverage   99.91%   99.91%   -0.01%     
==========================================
  Files         192      192              
  Lines       10027    10022       -5     
  Branches     1924     1922       -2     
==========================================
- Hits        10018    10013       -5     
  Partials        9        9              

☔ 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.

@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit 1dc4377 into main Aug 18, 2026
18 checks passed
@glennawatson
glennawatson deleted the fix/raw-header-value-preservation branch August 18, 2026 06:58
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]: raw header values are rewritten when a method declares more than one header

2 participants