---
title: "Fastify v5.9.0 Adds `request.mediaType` and `onMaxParamLength`, Hardens `forwarded` Header Trust, Chunks Large HTTP/2 Replies, and Moves Type Tests to TSTyche"
description: "Fastify v5.9.0, published on 2026-06-28 (github.com/fastify/fastify), is the first minor release of the v5 line in 2026 and a substantial 65-PR cycle. The headline features are `request.mediaType` (a typed accessor for the negotiated media type, [#6653](https://github.com/fastify/fastify/pull/6653) by climba03003), `onMaxParamLength` route option ([#6716](https://github.com/fastify/fastify/pull/6716) by climba03003), and a security fix that no longer trusts `X-Forwarded-Host` and `X-Forwarded-Proto` when the incoming socket is missing ([#6684](https://github.com/fastify/fastify/pull/6684) by mcollina). The cycle ships an HTTP/2 buffer-chunking fix for large replies ([#6746](https://github.com/fastify/fastify/pull/6746) by mcollina), three schema-related performance wins (deferred `getSchemaSerializer` content-type parsing #6692, cached `ContentType` objects in `ContentTypeParser` #6694, `typeof` guard before `toString.call` in `send` / `onSendEnd` #6693 by aquie00t), Node.js 26 added to the test matrix ([#6728](https://github.com/fastify/fastify/pull/6728) by Fdawgs) and Node.js 20 dropped from the yarn CI matrix ([#6662](https://github.com/fastify/fastify/pull/6662) by Tony133), the migration of the type-test suite from hand-rolled `expect-type` to [TSTyche](https://github.com/mrazauskas/tstyche) ([#6532](https://github.com/fastify/fastify/pull/6532) by mrazauskas, with follow-ups #6726 and #6727), and a TypeScript-only fastify-plugin v6.0.0 bump. Other notable fixes: trailer `res.end` deduplication (#6676), trailer duplicate-completion guard (#6714), `error.code` on routing errors (#6678), `hasRequestDecorator` / `hasReplyDecorator` catching constructor-assigned built-ins (#6753), `getValidationFunction()` allowed to return `undefined` (#6665), and a socket `_meta` clear that closes a keep-alive leak (#6799)."
date: 2026-07-01
image: "/images/heroes/2026-07-01--fastify-v5-9-0-security-perf-http2-buffer-chunking.png"
author: lschvn
tags: ["runtimes", "security"]
tldr:
  - "[Fastify v5.9.0](https://github.com/fastify/fastify/releases/tag/v5.9.0), published 2026-06-28, is the first minor of the v5 line in 2026. The two new public APIs are `request.mediaType` ([#6653](https://github.com/fastify/fastify/pull/6653)), a typed accessor that returns the negotiated media type from the `Accept` header (or `undefined` if none was negotiated), and the `onMaxParamLength` route option ([#6716](https://github.com/fastify/fastify/pull/6716)) which lets a route hook handle URLs whose single parameter exceeds the configured `maxParamLength` rather than throwing an `FST_ERR_VALIDATION` synchronously. Both are additive: no breaking changes to the public surface, and the v5 line stays in `0.x` of semver patch."
  - "The cycle ships a security fix that closes a request-spoofing gap: [#6684](https://github.com/fastify/fastify/pull/6684) (mcollina) refuses to trust `X-Forwarded-Host` and `X-Forwarded-Proto` when the incoming socket is missing (for example behind a transport that strips the source connection), forcing the route to fall back to the raw `request.host` instead. The same fix flags the `request` metadata accessors as `untrusted input` in the TypeScript declaration file ([#6572](https://github.com/fastify/fastify/pull/6572)). The release also adds `useContentType`-as-schema-key support so the response schema lookup honors the negotiated content type ([#6685](https://github.com/fastify/fastify/pull/6685) by UlisesGascon), and three other small hardening commits around routing error codes ([#6678](https://github.com/fastify/fastify/pull/6678)) and `checkDependencies` ([#6774](https://github.com/fastify/fastify/pull/6774))."
  - "Performance work clusters around content-type parsing on the hot path. [#6692](https://github.com/fastify/fastify/pull/6692) (aquie00t) defers the `ContentType` parse inside `getSchemaSerializer` until the schema is actually read, [#6694](https://github.com/fastify/fastify/pull/6694) caches parsed `ContentType` objects inside the `ContentTypeParser`, and [#6693](https://github.com/fastify/fastify/pull/6693) adds a `typeof` guard in front of `toString.call` in `send` and `onSendEnd`. The release also chunks large HTTP/2 buffer replies to keep memory bounded under big payloads ([#6746](https://github.com/fastify/fastify/pull/6746) by mcollina). The type-test suite is migrated from hand-rolled `expect-type` assertions to [TSTyche](https://github.com/mrazauskas/tstyche) ([#6532](https://github.com/fastify/fastify/pull/6532) by mrazauskas, plus #6726 and #6727), and Node.js 26 is added to the test matrix ([#6728](https://github.com/fastify/fastify/pull/6728) by Fdawgs) while Node.js 20 is dropped from the yarn matrix ([#6662](https://github.com/fastify/fastify/pull/6662) by Tony133)."
faq:
  - question: "What is `request.mediaType` and why is it new in v5.9.0?"
    answer: "`request.mediaType` is a typed accessor on the Fastify request that returns the negotiated media type from the `Accept` header, or `undefined` if no media type was negotiated. Before v5.9.0, application code parsed `request.headers['accept']` by hand, walked the q-value list, picked the highest match, and then compared to a `Content-Type` it hoped the route served. The new accessor (added by [PR #6653](https://github.com/fastify/fastify/pull/6653) by climba03003) does the parse and the match in one place. The implementation lives in `lib/request.js` and the TypeScript surface is exported in `fastify.d.ts`. For a `req` that asked for `application/json`, the accessor returns `'application/json'`; for a `req` with no `Accept` header, it returns `undefined`. The accessor is additive, no migration needed for existing routes that read the `Accept` header themselves, but new code is meant to use it."
  - question: "What does the `onMaxParamLength` option do?"
    answer: "`onMaxParamLength` is a per-route hook that fires when a single URL parameter exceeds the configured `maxParamLength` (default 100 characters). Without the option, Fastify throws an `FST_ERR_VALIDATION` synchronously. With the option, the route can do something with the offending request (log a warning, increment a metric, redirect, or return a custom error) instead of a hard throw. The hook is added by [PR #6716](https://github.com/fastify/fastify/pull/6716) (climba03003). The option is a normal route option (`{ onMaxParamLength: (req, reply, maxParamLength) => void }`), so it lives next to `preHandler`, `onRequest`, and the rest of the lifecycle hooks. The change is additive: routes that did not set the option keep the old behavior, and the option is off by default."
  - question: "What does the `forwarded` header trust fix actually change?"
    answer: "[PR #6684](https://github.com/fastify/fastify/pull/6684) (mcollina) closes a request-spoofing gap in the forwarded-header trust path. Before the fix, Fastify would trust the `X-Forwarded-Host` and `X-Forwarded-Proto` headers even when the incoming `request.socket` was missing or undefined (a situation that can happen behind a transport that strips the source connection, or when the request is reconstructed from a parsed body). The new code refuses to read the forwarded headers in that case, falls back to the raw `request.host`, and surfaces the situation as `untrusted input` in the TypeScript declaration file via [PR #6572](https://github.com/fastify/fastify/pull/6572) (mcollina, which marks the `request` metadata accessors (`request.ip`, `request.host`, `request.protocol`, `request.ips`) with explicit JSDoc warnings that they are not to be trusted for security decisions). The fix is invisible to correctly configured proxies (which always have a real socket on the incoming request) and the change is on the safe side: missing socket means no forwarded header, even if the header is present in the request."
  - question: "What is the HTTP/2 buffer-chunking fix about?"
    answer: "[PR #6746](https://github.com/fastify/fastify/pull/6746) (mcollina) addresses a memory issue in the HTTP/2 reply path. Before the fix, a route that returned a very large payload (the kind of size you would see in a streaming export or a generated binary blob) would buffer the entire payload in memory before sending it over the HTTP/2 connection. For a 1 GB export, that meant 1 GB of resident memory on the server. The fix chunks the buffer into smaller pieces and streams them as the HTTP/2 connection can absorb them, so the memory cost is bounded by the chunk size rather than the payload size. The change is invisible to application code; it lives in the HTTP/2 send path. Routes that returned small payloads see no change. Routes that returned very large payloads now hold a bounded amount of memory regardless of payload size."
  - question: "Why is the move to TSTyche for type tests interesting?"
    answer: "Fastify has a long-running type-test suite that asserts the public TypeScript surface is shaped the way the maintainers documented. The suite was previously built on hand-rolled `expect-type` assertions, which are a thin runtime check. [PR #6532](https://github.com/fastify/fastify/pull/6532) (mrazauskas) migrates the suite to [TSTyche](https://github.com/mrazauskas/tstyche), a purpose-built type-test runner that runs at TypeScript's compile step rather than at runtime, and that gives better diagnostics when an assertion fails (the failure points at the line and the expected vs. actual type, rather than a runtime `false` from `expect-type`). Follow-ups [#6726](https://github.com/fastify/fastify/pull/6726) and [#6727](https://github.com/fastify/fastify/pull/6727) finish the migration in two batches. The change is invisible to Fastify users; it speeds up the maintainers' CI and gives them better error messages when a type test fails."
  - question: "What changed in the Fastify v5.9.0 dependency footprint?"
    answer: "The release pins a `fastify-plugin` bump from 5.1.0 to 6.0.0 ([#6801](https://github.com/fastify/fastify/pull/6801)), which is a TypeScript-only major. The `concurrently` dev dependency is bumped to 10.0.0 ([#6752](https://github.com/fastify/fastify/pull/6752)), `actions/checkout` is bumped to 7 ([#6812](https://github.com/fastify/fastify/pull/6812)), `actions/github-script` is bumped from 8 to 9 ([#6705](https://github.com/fastify/fastify/pull/6705)), and `pnpm/action-setup` is bumped to 6.0.4 ([#6704](https://github.com/fastify/fastify/pull/6704)). Node.js 20 is dropped from the yarn CI matrix ([#6662](https://github.com/fastify/fastify/pull/6662) by Tony133) and Node.js 26 is added to the test matrix ([#6728](https://github.com/fastify/fastify/pull/6728) by Fdawgs), which lands Fastify's CI on the [Node.js 26 line](https://nodejs.org/en/blog/release/v26.4.0) that the Node.js team put into current in June."
  - question: "Is Fastify v5.9.0 safe to upgrade to today?"
    answer: "Yes. The release is the first minor of the v5 line in 2026, the public surface has no breaking changes, and the security fix in [PR #6684](https://github.com/fastify/fastify/pull/6684) is on the safe side (it refuses to trust forwarded headers when the socket is missing). The only people who should pause are teams that depend on a specific `forwarded`-header behavior in synthetic test setups that simulate a request with no socket, so those tests will now see `request.host` fall back to the raw header instead of the forwarded value, which is the correct behavior but a behavior change. For everyone else, `npm install fastify@5.9` (or the major version's current minor) is a drop-in."
---

[Fastify v5.9.0](https://github.com/fastify/fastify/releases/tag/v5.9.0), published on 2026-06-28, is the first minor release of the Fastify v5 line in 2026 and a substantial 65-PR cycle. The release's two new public APIs are [`request.mediaType`](https://github.com/fastify/fastify/pull/6653), a typed accessor for the negotiated media type, and the [`onMaxParamLength`](https://github.com/fastify/fastify/pull/6716) route option, which lets a route hook handle URLs whose single parameter exceeds the configured `maxParamLength` rather than throwing an `FST_ERR_VALIDATION` synchronously. The cycle also ships a security fix that no longer trusts `X-Forwarded-Host` and `X-Forwarded-Proto` when the incoming socket is missing ([#6684](https://github.com/fastify/fastify/pull/6684) by mcollina), chunks large HTTP/2 buffer replies ([#6746](https://github.com/fastify/fastify/pull/6746)), migrates the type-test suite from hand-rolled `expect-type` assertions to [TSTyche](https://github.com/mrazauskas/tstyche) ([#6532](https://github.com/fastify/fastify/pull/6532)), and adds Node.js 26 to the test matrix while dropping Node.js 20 from the yarn matrix.

The release is the third minor of the v5 line this year. [Fastify v5.8.0](https://github.com/fastify/fastify/releases/tag/v5.8.0) shipped on March 5, 2026, and [v5.8.5](https://github.com/fastify/fastify/releases/tag/v5.8.5) was the last 5.8.x on April 14, 2026. v5.9.0 is the first one since that 5.8.5 patch that ships a new public API.

## The two new public APIs

[`request.mediaType`](https://github.com/fastify/fastify/pull/6653) (climba03003) is the headline feature. Before v5.9.0, application code that wanted to know what media type the client asked for parsed `request.headers['accept']` by hand, walked the q-value list, picked the highest match, and then compared to a `Content-Type` it hoped the route served. The new accessor does the parse and the match in one place. For a `req` that asked for `application/json`, the accessor returns `'application/json'`; for a `req` with no `Accept` header, it returns `undefined`. The accessor is additive, and the public surface is unchanged. The implementation lives in `lib/request.js` and the TypeScript surface is exported in `fastify.d.ts`.

[`onMaxParamLength`](https://github.com/fastify/fastify/pull/6716) (climba03003) is the second. Without the option, Fastify throws an `FST_ERR_VALIDATION` synchronously when a single URL parameter exceeds the configured `maxParamLength` (default 100 characters). With the option, the route can do something with the offending request: log a warning, increment a metric, redirect, or return a custom error. The hook is a normal route option (`{ onMaxParamLength: (req, reply, maxParamLength) => void }`), sitting next to `preHandler`, `onRequest`, and the rest of the lifecycle hooks. The change is additive: routes that did not set the option keep the old behavior, and the option is off by default.

## The security fix and the related work

[PR #6684](https://github.com/fastify/fastify/pull/6684) (mcollina) closes a request-spoofing gap in the forwarded-header trust path. Before the fix, Fastify would trust the `X-Forwarded-Host` and `X-Forwarded-Proto` headers even when the incoming `request.socket` was missing or undefined (a situation that can happen behind a transport that strips the source connection, or when the request is reconstructed from a parsed body in a test). The new code refuses to read the forwarded headers in that case, falls back to the raw `request.host`, and surfaces the situation as `untrusted input` in the TypeScript declaration file via [PR #6572](https://github.com/fastify/fastify/pull/6572) (mcollina), which marks the request metadata accessors (`request.ip`, `request.host`, `request.protocol`, `request.ips`) with explicit JSDoc warnings that they are not to be trusted for security decisions. The fix is invisible to correctly configured proxies (which always have a real socket on the incoming request) and the change is on the safe side: missing socket means no forwarded header, even if the header is present in the request.

The release also adds `useContentType`-as-schema-key support so the response schema lookup honors the negotiated content type ([#6685](https://github.com/fastify/fastify/pull/6685) by UlisesGascon), three small hardening commits around routing error codes ([#6678](https://github.com/fastify/fastify/pull/6678)) and `checkDependencies` ([#6774](https://github.com/fastify/fastify/pull/6774)), and marks `request` metadata accessors as untrusted input in the TypeScript declaration file ([#6572](https://github.com/fastify/fastify/pull/6572) by mcollina).

## Performance work clusters on the content-type hot path

Three of the four perf entries of the cycle live on the same hot path. [PR #6692](https://github.com/fastify/fastify/pull/6692) (aquie00t) defers the `ContentType` parse inside `getSchemaSerializer` until the schema is actually read, [#6694](https://github.com/fastify/fastify/pull/6694) (aquie00t) caches parsed `ContentType` objects inside the `ContentTypeParser`, and [#6693](https://github.com/fastify/fastify/pull/6693) (aquie00t) adds a `typeof` guard in front of `toString.call` in `send` and `onSendEnd`. The fourth is the [HTTP/2 buffer-chunking fix](https://github.com/fastify/fastify/pull/6746) (mcollina) for large replies: the old code buffered the entire payload in memory before sending it over the HTTP/2 connection (a 1 GB export meant 1 GB of resident memory on the server); the new code chunks the buffer into smaller pieces and streams them as the HTTP/2 connection can absorb them, so the memory cost is bounded by the chunk size rather than the payload size.

The [Vite 8.1 article from June 24](/articles/2026-06-24--vite-8-1-stable-bundled-dev-mode) covered a similar shape of perf work in the dev-server build path: a small change on a hot path, repeated across the request lifecycle, that adds up to a noticeable improvement. The Fastify v5.9.0 work is the same idea, on a different runtime.

## Trailer, validation, and other fixes

The cycle ships a cluster of fixes around the trailer and validation paths. [#6676](https://github.com/fastify/fastify/pull/6676) (climba03003) prevents a duplicate `res.end` in `sendTrailer` when a sync callback fires the trailer, and [#6714](https://github.com/fastify/fastify/pull/6714) (mcollina) ignores duplicate trailer completions. [#6678](https://github.com/fastify/fastify/pull/6678) (mcollina) restores the `error.code` on routing errors that were silently dropping it, and [#6665](https://github.com/fastify/fastify/pull/6665) (trivikr) allows `request.getValidationFunction()` to return `undefined` in the TypeScript declaration, matching the actual runtime behavior. [#6753](https://github.com/fastify/fastify/pull/6753) (LeSingh1) makes `hasRequestDecorator` and `hasReplyDecorator` catch constructor-assigned built-in properties that were previously missed, which closes a long-standing gap where the decorators were wrongly reported as not present on a request or reply that had been wrapped by a built-in.

The release also ships a Node.js test-matrix bump. [#6728](https://github.com/fastify/fastify/pull/6728) (Fdawgs) adds Node.js 26 to the test matrix, which lines up with the [Node.js 26.4.0 release on June 25](/articles/2026-06-25--node-js-26-4-current-vfs-loader-package-maps), and [#6662](https://github.com/fastify/fastify/pull/6662) (Tony133) drops Node.js 20 from the yarn CI matrix, which is consistent with the Node.js project's [End-of-Life schedule for Node.js 20](https://nodejs.org/en/about/previous-releases) and with the [Deno 2.9 article from June 26](/articles/2026-06-26--deno-2-9-cold-start-supply-chain-tests) covering the runtime landscape.

## TSTyche for type tests, fastify-plugin v6.0.0

The migration of Fastify's type-test suite from hand-rolled `expect-type` assertions to [TSTyche](https://github.com/mrazauskas/tstyche) is the most interesting infrastructure change of the cycle. [PR #6532](https://github.com/fastify/fastify/pull/6532) (mrazauskas) introduces TSTyche, and [#6726](https://github.com/fastify/fastify/pull/6726) and [#6727](https://github.com/fastify/fastify/pull/6727) finish the migration in two batches. TSTyche is a purpose-built type-test runner that runs at TypeScript's compile step rather than at runtime, and that gives better diagnostics when an assertion fails (the failure points at the line and the expected vs. actual type, rather than a runtime `false` from `expect-type`). The change is invisible to Fastify users; it speeds up the maintainers' CI and gives them better error messages when a type test fails.

The release also pins a `fastify-plugin` bump from 5.1.0 to 6.0.0 ([#6801](https://github.com/fastify/fastify/pull/6801)). `fastify-plugin` is a TypeScript-only peer of Fastify; v6.0.0 is a major version bump that drops some of the older CommonJS-style code paths and aligns the API surface with the v5 Fastify line. The bump is on a `dependencies` line that plugin authors consume via `peerDependencies`; application code that uses Fastify directly is unaffected.

## Why this matters

Fastify v5.9.0 is the release where the framework is mid-way through a year-long hardening cycle. The v5.0 line shipped in late 2024 and the project has been steadily adding type-safe accessors, hardening the forwarded-header trust path, and migrating the test infrastructure to tools that are a better fit for the codebase. v5.9.0 is the first release in that cycle that ships both a new public API and a security-relevant fix, which is the pattern that makes a minor worth writing about.

For application developers the practical change is small and concrete. The two new public APIs are additive and the security fix is on the safe side. The TypeScript surface gets a new accessor and the `request` metadata accessors are explicitly marked as untrusted input, which is the right thing to do and the right time to do it. The HTTP/2 buffer-chunking fix is invisible to small payloads and significant for very large ones, which is the shape of fix that matters for a route that serves a generated binary blob.

For the wider Node.js web framework space, Fastify v5.9.0 lines up with the [Bun runtime updates](/articles/2026-04-13--bun-1-3-12-webview-browser-automation-using-await-using) and the [Deno 2.9 release notes](/articles/2026-06-26--deno-2-9-cold-start-supply-chain-tests) to show that the three major JavaScript runtimes are all hardening their HTTP and request-handling paths in parallel. Fastify is the framework-level counterpart to those runtime-level changes.
