Skip to content

perf: avoid redundant request-part reads during validation - #7018

Merged
gurgunday merged 1 commit into
mainfrom
perf/avoid-redundant-request-reads
Sep 11, 2026
Merged

gurgunday merged 1 commit into
mainfrom
perf/avoid-redundant-request-reads

Conversation

@gurgunday

Copy link
Copy Markdown
Member

Before the patch, validation could read headers twice despite having no header validator. If application code had previously assigned request.headers = {...}, those reads copied the merged headers twice in this path. You can see why we copied in the headers getter here:

fastify/lib/request.js

Lines 277 to 286 in af079bd

headers: {
get () {
if (this.additionalHeaders) {
return Object.assign({}, this.raw.headers, this.additionalHeaders)
}
return this.raw.headers
},
set (headers) {
this.additionalHeaders = headers
}

The patch checks for a validator before reading the request part and reads validated parts once. Query-only validation therefore reduces header copies from two to zero; header validation reduces them from two to one.

Header assignment may occur in onRequest, preParsing, a content-type parser, or preValidation, including encapsulated hooks.

   Scenario                                                  Before            After    Change
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━
   Query validation + enriched headers                 51,456 req/s     94,349 req/s      +83%
  ─────────────────────────────────────────────────  ───────────────  ───────────────  ────────
   Query + header validation with enriched headers     51,059 req/s     64,957 req/s      +27%
  ─────────────────────────────────────────────────  ───────────────  ───────────────  ────────
   Query validation only                              147,968 req/s    147,024 req/s    −0.64% (flat)

@gurgunday gurgunday added the performances Everything related to performances label Sep 10, 2026
@gurgunday
gurgunday requested a review from a team September 10, 2026 10:08

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@mcollina mcollina added the backport 5.x Issue or pr that should be backported to Fastify v5 label Sep 10, 2026
@gurgunday

This comment was marked as resolved.

@jean-michelet

jean-michelet commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Edit: Ok seems like next is behind anyway and main is v6

Yep, we push on main and backport to 5.x when non-breaking.

@gurgunday
gurgunday merged commit d266f83 into main Sep 11, 2026
39 checks passed
@gurgunday
gurgunday deleted the perf/avoid-redundant-request-reads branch September 11, 2026 08:48
climba03003 pushed a commit that referenced this pull request Sep 11, 2026
Before the patch, validation could read headers twice despite having no
header validator. If application code had previously assigned
`request.headers = {...}`, those reads copied the merged headers twice
in this path. You can see why we copied in the headers getter here:

https://github.com/fastify/fastify/blob/af079bd4c60c3cbebedc7640517d7288468fb5eb/lib/request.js#L277-L286

The patch checks for a validator before reading the request part and
reads validated parts once. Query-only validation therefore reduces
header copies from two to zero; header validation reduces them from two
to one.

Header assignment may occur in onRequest, preParsing, a content-type
parser, or preValidation, including encapsulated hooks.

```text
   Scenario                                                  Before            After    Change
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━  ━━━━━━━━
   Query validation + enriched headers                 51,456 req/s     94,349 req/s      +83%
  ─────────────────────────────────────────────────  ───────────────  ───────────────  ────────
   Query + header validation with enriched headers     51,059 req/s     64,957 req/s      +27%
  ─────────────────────────────────────────────────  ───────────────  ───────────────  ────────
   Query validation only                              147,968 req/s    147,024 req/s    −0.64% (flat)
```

Signed-off-by: Gürgün Dayıoğlu <[email protected]>
(cherry picked from commit d266f83)
gurgunday added a commit that referenced this pull request Sep 11, 2026
…tion (#7020)

Backport d266f83 from #7018.

Signed-off-by: Gürgün Dayıoğlu <[email protected]>
Co-authored-by: Gürgün Dayıoğlu <[email protected]>
hktitof added a commit to hktitof/fastify that referenced this pull request Sep 22, 2026
the trust proxy host getter reads request.headers twice per access, once
for the x-forwarded-host check and once for the host fallback. the
protocol getter does the same on its x-forwarded-proto branch. every read
of that getter copies the full merged header set when request.headers was
assigned in a hook, so one request.host or request.protocol read copied
the headers twice. fastify#7018 fixed the same double read in the validation path

the getters now read the merged headers once and reuse that object, so a
read copies the headers at most once. the returned values are unchanged

Signed-off-by: Abdel <[email protected]>
jean-michelet pushed a commit that referenced this pull request Sep 22, 2026
> **Context:** Sibling to #7018 (which stopped validation from reading
request parts when no validator exists). This addresses the trust proxy
host and protocol getters in lib/request.js (request.js:120-148), which
still read the copying request.headers getter twice per access.

the trust proxy request class defines its own host and protocol getters
that honor x-forwarded-host and x-forwarded-proto when the proxy is
trusted. the host getter reads this.headers twice per access, once for
the x-forwarded-host check and once for the host fallback, and the
protocol getter pays the same price on its x-forwarded-proto branch.
every read of the request.headers getter copies the full merged header
set when request.headers was assigned in a hook (onRequest, preParsing,
a content-type parser or preValidation, same list #7018 documented)

so with a hook like

```js
fastify.addHook('onRequest', (request, reply, done) => {
  request.headers = { ...request.headers, 'x-request-id': id }
  done()
})
```

one request.host access copied all headers twice, request.hostname
inherits it through this.host and request.protocol pays the same price
on its own branch. #7018 already fixed the validation side of this, this
PR applies the same rule to the getters themselves: read this.headers
once and reuse that object, so an access copies the merged headers at
most once

the returned values are unchanged in every case, this is a pure
allocation reduction, no default or precedence moves. the regular (non
trust proxy) host getter already reads raw.headers directly and needs no
change

testing proof:

- new tests in test/internals/request.test.js count the reads of the
merged headers getter through an own accessor on the request and assert
one copy per access, one test for the host getter and one for the
protocol getter: each fails on main with 2 !== 1 and passes with the fix
(16/16 in that file)
- a live probe on untouched main shows one request.host read copies the
merged headers twice, once with the fix, values identical in both cases
- adjacent internals stay green: test/internals/handle-request.test.js +
test/internals/request-validate.test.js → 51/51
- full `npm run unit` 2343 tests, 0 failures, `npm run test:types` 1282
assertions pass, `npx eslint lib/request.js
test/internals/request.test.js` clean
- one commit, Signed-off-by present (DCO)

---

This change was developed with AI assistance. The analysis,
reproduction, red/green test evidence, and final diff were reviewed by
the branch author before opening this pull request.

Signed-off-by: Abdel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport 5.x Issue or pr that should be backported to Fastify v5 performances Everything related to performances

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants