NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #1783 most downloaded on npm
Minimal H(TTP) framework built for high performance and portability.
Last release today
03 Oct 2026
Ships fairly regularly
a new release about every 3 weeks
Most releases are documented
notes for 43 of the last 60 stable releases
3 versions withdrawn
withdrawn after publishing
10 years old
153 releases · first in 2016
Read the migration guide for the breaking changes and their new equivalents.
H3 v2 is now stable! 🎉
Note
A full release article is coming soon on the H3 blog.
H3 v2 is a full rewrite on top of web standards (Request, Response, URL, Headers). It stays compatible with most v1 utilities and is faster and smaller than v1.
👉 Get started: https://h3.dev/guide
👉 Upgrading from v1: https://h3.dev/migration
event.req (a standard Request) and event.url, and set response headers through event.res. Return any value (string, object, Response, stream, Blob, ...) and H3 converts it to a Response.event.req.runtime gives access to runtime-specific details when needed.handler.fetch().(event, next) middleware, built-in helpers (onRequest, onResponse, onError, basicAuth, bodyLimit, ...) and reusable definePlugin() extensions.HTTPError), plus validated handlers and body/query/params validation with any Standard Schema library.h3/rules): Add headers, redirects, CORS, caching and proxying to groups of routes with one config object. See the Route Rules guide.h3/tracing plugin. See https://h3.dev/utils.>= 20.19 (if you run on Node.js)Most v1 utilities keep working, and deprecated v1 names are still exported to make upgrading easier. Read the migration guide for the breaking changes and their new equivalents.
Thank you to everyone who tested the betas and release candidates, reported issues, and sent PRs, and to the Nitro community for using v2 throughout its development. ❤️
For background on the v2 design, see the v2 beta announcement.
One column per quarter.
/foo/ /foo/a/b { _: "a/b" } { 0: "a/b", _: "a/b" } ( _ is deprecated)
Important
This release upgrades the router to rou3 v1 (from v0.9). Route patterns now follow URLPattern syntax more closely, and some patterns match differently. Please read the routing changes section below before you upgrade. The full guide is the rou3 migration guide.
These changes apply to app.get() / app.on() / ... routes, app.use(route, ...) middleware scopes, mount(), and routeRules keys alike.
* in your routes. It now spans segments: /admin/* matches /admin/a/b. Use :name to match exactly one segment.params._ with params[0], or name the catch-all (/**:path → params.path). On the base path (/foo for /foo/**), the key is now left out instead of being "".- (:user-id → :userId).rou3: ... error when you register them. The error quotes the route.>=20.19.0).| Route | Request | rc.32 | rc.33 |
|---|---|---|---|
/users/* |
/users/a/b |
404 | { 0: "a/b" } |
/foo/** |
/foo |
{ _: "" } |
{} |
/foo/** |
/foo/a/b |
{ _: "a/b" } |
{ 0: "a/b", _: "a/b" } (_ is deprecated) |
/about |
/about// |
match | 404 (only one trailing slash is ignored) |
/u/:id/x |
/u//x |
{ id: "" } |
404 (:name never matches an empty segment) |
/api/:user-id |
/api/abc-id |
{ "user-id": "abc-id" } |
{ user: "abc" } (- ends a param name) |
/f/:name.:ext |
/f/a.tar.gz |
{ name: "a.tar", ext: "gz" } |
{ name: "a", ext: "tar.gz" } |
/d/:a-:b |
/d/x-y-z |
{ "a-": "x-y-", b: "z" } |
{ a: "x", b: "y-z" } |
* is a greedy catch-all. This also widens middleware and route rules: app.use("/admin/*", guard) and routeRules({ "/admin/*": ... }) now apply at every depth below /admin, not just one level. To match exactly one segment, use /admin/:id (or /admin/:id? to also match /admin). Inside a segment, replace * with ([^\x2f]*), so /*.png becomes /([^\x2f]*).png.(.*) and :name(.*) are catch-alls too, like *.** are matched. /**/_payload.json matches only paths that end in /_payload.json. Before, those trailing segments were ignored.*, **, :x+, :x*, (.*) or :x(.*). For example, /**/*.png now throws; write /**/:file.png instead.:name takes as little as it can. To keep the old split, add a constraint, e.g. /:name.:ext(\w+).prefix-:param? makes only the param optional, not the whole segment. Write /a/{pre-:x}? to make the whole segment optional./foo\.bar matches /foo.bar.+ / * after a constrained or prefixed param (/a/:x(\d+)+), invalid param names (/:0, /:id$, /:café), duplicate param names, unbalanced braces or parens, look-arounds / anchors / capturing groups inside a regex constraint, and a dot segment next to a param. See Patterns that now throw.InferRouteParams reads param names the same way as the router. Optional params and a trailing * / ** are typed string | undefined.h3/rules)MatchedRouteRule.route and the default cache rule name read /caf%C3%A9/** for a "/café/**" key. (7cbf21c)* is now a catch-all, a redirect or proxy key such as /x/*/old/** has two catch-alls and is rejected when the rules are created. Use :param for the single segment./docs/x%2fy decoded to /docs/x/y) can bring back a permission that a broader false reset removed, but only from a pattern that is equal to or more specific than every pattern that reset it. Restricting custom handlers should keep setting restricting: true. (3f1c9e5)See the updated Routing and Route Rules guides.
redirect when the request is already at the target, so "/docs/**": { redirect: "/docs/v2/**" } no longer loops (#1559)/api//admin) with 404. Before, the path was collapsed to /admin, which reached the mounted app while skipping use("/api/admin/**") guards on the parent. This also applies to withBase() (1161eb7)defineValidatedHandler. Query validators now receive string | string[], the same shape as getQuery (#1562)partitioned in the distinct-cookie key (#1553)accept-encoding (#1560)vary header when a single encoding is accepted (#1556).mjs and .cjs to the common MIME types (#1566)headers: false in RouteRuleConfig (edcc329)event.context.basicAuth.username and realm are always set after requireBasicAuth (#1550)EventHandlerRequest["query"] accepts string | string[] values (#1562)event.url.search (#1551)redirect when the request is already at the target (#1559)headers: false in RouteRuleConfig (edcc329)// after base (1161eb7)Export normalizeRoute as a public path utility
normalizeRoute as a public path utility (#1549)TypedHeaders type (#1547)** in redirect/proxy targets (228bca3)normalizeRoute as a public path utility (#1549)status in HTTPError.isError (e6b726b)TypedHeaders type (#1547)req.headers (ae529f1)rules: Support non-trailing in redirect/proxy targets
** in redirect/proxy targets (228bca3)session: Do not persist a session that is only read
ws: Keep WebSocket hooks reachable when the response is rebuilt
static: Keep a leading separator run from bypassing a route guard
onError hook (2d6a10a)HTTPResponse by brand instead of constructor.name (0bbcbc4)onError hook again (f176b35)HTTPResponse by brand instead of constructor.name (0bbcbc4)onError hook again (f176b35)New route rules engine ( #1524 ) ( docs )
idleTimeout for sliding expiration (#1513)use() route filters with rou3 (d9d3124)status and statusText (8e69593)SHA-256 and disambiguate components (51e68cd)x-forwarded-* headers win (0c7429e)x-forwarded-host from stripping the real port (429b994)removeRoute from unregistering sibling routes (94d0edd)x-forwarded-* headers win (0c7429e)x-forwarded-* headers win (0c7429e)deprecated: Correct v1 signatures in the compat shim
mergeSlashes option (9581407)SameSite=Lax (acf8d77)onDispose hook (#1488)resolveDotSegments (#1458)setChunkedCookie (#1469)raw() trust marker unforgeable and hoist escape map (#1473)-32600 for valid-JSON non-object bodies (#1483)event.context and req.context as one reference (#1499)onResponse hook (4a32c1b)prepareResponse throws through the error pipeline (#1503)content-length header for Uint8Array responses (#1504)HEAD body when merging prepared headers into a mutable Response (#1490)x-forwarded-proto trust opt-in (#1461)createEventStream (#1509)event.url.pathname decoding (3f8b5bc)mergeSlashes option (9581407)html tagged template (#1459)onDispose hook (#1488)resolveDotSegments (#1458)null origin (#1464)-32600 for valid-JSON non-object bodies (#1483)autoclose option (#1495)event.url.pathname decoding (3f8b5bc)H3Core types (978b17c)html tagged template (#1459)autoclose option (#1495)cors: Correct vary handling and credentialed wildcard behavior
@__PURE__ annotations (b17c451)Add requireContentType and appendAcceptQuery utils
requireContentType and appendAcceptQuery utils (#1446)decode:true from reintroducing path separators (cd03d41)resolveDotSegments as a public path utility (#1428)requireContentType and appendAcceptQuery utils (#1446)if (validate.body) guard in body proxy (#1392)QUERY method docs (#1447).crossws on defineWebSocketHandler return type (#1435)/* @__PURE__ */ comment (c98f2d0)ws: Allow optional HTTP handling in defineWebSocketHandler
defineWebSocketHandler (#1425)resolveDotSegments as a public path utility (#1428, #1430)formdata type (#1164)NaN (#1420)handleCacheHeaders ignores multi-value If-None-Match header (#1395)x-forwarded-proto header (#1413)_url in requestWithURL proxy (d21d93c)_url (a1cf066)req.url with event.url in fromNodeHandler (#1433)if (validate.body) guard in body proxy (#1392).crossws on defineWebSocketHandler return type (#1435)\_url normalization semantics (4a218a8)req.url reflects normalization per runtime (8410ec9)resolveDotSegments as a public path utility (#1428)if (validate.body) guard in body proxy (#1392).crossws on defineWebSocketHandler return type (#1435)/* @__PURE__ */ comment (c98f2d0)tracing: Export wrapHandlerWithTracing for manual handler wrapping
wrapHandlerWithTracing for manual handler wrapping (#1369).handler (a94b7fb)Coerce thrown number/string to HTTPError
encode and stringify serialize options (#1377)encode and stringify serialize options (#1377)Pass single obj to serializeCookie
body: Enforce stream-based body size check regardless of content-length header
await-thenable lint rule and fix invalid await usage (#1353)await-thenable lint rule and fix invalid await usage (#1353)utils: Prevent open redirect via protocol-relative path in redirectBack()
redirectBack() (459a1c6)cors: Preserve CORS headers on error responses
Allow header in 405 response (#1314)requestWithBaseURL (0295f90)startsWith check (7ccc9e2)Allow header in 405 response (#1314)requestWithBaseURL (0295f90)startsWith check (7ccc9e2)Update rou3 to 0.8 (4701dc4) (release notes)
handler: New defineJsonRpcHandler and defineJsonRpcWebSocketHandler
defineJsonRpcHandler and defineJsonRpcWebSocketHandler (#1180)h3 docs (#1311)defineJsonRpcHandler and defineJsonRpcWebSocketHandler (#1180)h3 docs (#1311)ESNext to tsconfig's lib (#1297)tracing: Rename tracing channel .fetch to .request
.fetch to .request (#1294)compare changes ### 💅 Refactors - Improve cli
writeEarlyHints: Add Link: rel:preload headers as fallback
headers are frozen (#1287)event.res.headers.set (#1289)headers are frozen (#1287)event.res.headers.set (#1289)compare changes ### 📦 Build - Fix types bundling
Move fetchdts to dependencies due to bundle issues
basic-auth: Use jitter and constant-time string comparison
fromNodeHandler: Pipe responses once
Error.captureStackTrace (652e883)> srvx has been updated to v0.10.. Please review the release notes for important information about Node.js compatibility.
[!IMPORTANT] srvx has been updated to v0.10.. Please review the release notes for important information about Node.js compatibility.
duplex option is properly set (eb83aad)modifiedTime to seconds (#1262)duplex option is properly set (eb83aad)modifiedTime to seconds (#1262)defineWebSocketHandler: Support callback with event
event (#1242)transfer-encoding header from proxied response (#1248)event.res after prepare (#1259)event (#1242)transfer-encoding header from proxied response (#1248)event.res after prepare (#1259)toMiddleware util (#1234, 0f2e568)
content-length and transfer-encoding headers (9ccd301).pathname for mounted sub-app routed middleware (#1232)toMiddleware util (#1234)content-length and transfer-encoding headers (9ccd301)toMiddleware (0f2e568)RouterContext and MatchedRoute types (fd7dc8f)_getMiddleware route typed (e090a76)H3Core (#1233)Add assertBodySize util and bodyLimit middleware
assertBodySize util and bodyLimit middleware (#1222)Freeze default response headers
[!NOTE] See srvx@0.9 release notes.
Response is not ok (#1228)body in HTTPError.toJSON result (#1216)defineEventHandler, eventHandler, lazyEventHandler (f185ce6)Response is not ok (#1228)body in HTTPError.toJSON result (#1216)defineEventHandler, eventHandler, lazyEventHandler (f185ce6)Response is not ok (#1228)Deprecate and move toNodeHandler to h3/node
toNodeHandler to h3/node (#1215)Nothing published for this version
Nothing published for this version
Try avoid cloning response for meriging headers
HTTPResponse (#1212)event._res and event.res._headers (#1185)HTTPResponse (#1212)HTTPResponse (#1212)event._res and event.res._headers (#1185)HTTPResponse (#1212)Support universal { fetch } handlers
{ fetch } handlers (#1210)toEventHandler and HTTPHandler (38be512)getRequestIP performances (#1197){ fetch } handlers (#1210)toEventHandler and HTTPHandler (38be512)toMiddleware and defineLazyMiddleware utils (2737f62)getRequestIP performances (#1197)event to _getMiddleware (3f766a5)toMiddleware and defineLazyMiddleware (unreleased) (f16f954)ProxyOptions separately (#1199)pnpm-lock.yaml (8f75d2f)Adopt srvx req.context and ServerRequestContext
req.context and ServerRequestContext (#1194)HTTPEvent for more agnostic usage (#1195)toRequest utility (55a2c9b)H3RouteMeta in RouteDefinition type (#1181)Response for no-content (#1177)req.context and ServerRequestContext types (#1194)HTTPEvent for more agnostic usage (#1195)toRequest (55a2c9b)H3RouteMeta in RouteDefinition type (#1181)Response for no-content (#1177)srvx (#1186)BufferSource (9040219)app.request(unicode) (1f874ee)⚠️ Only accept req for H3.fetch and EventHandlerWithFetch.fetch
app.request (#1176)req for H3 and EventHandlerWithFetch fetch (#1096)Allow passthrough response in middleware without enforcing explicit return
defineRoute (#1143)HTTPError check (#1145)EventHandlerObject props (#1147)noContent example (#1171)defineRoute (#1143)HTTPError check (#1145)EventHandlerObject props (#1147)Route Meta (#1118) (docs, docs)
H3Event.app?.{fetch,config} (#1139)H3Core (#1127)readMultipartFormData for backward compatibility (#1120)req.headers (#1130)toResponse util (febb832)fetch + event.app.fetch (#1141)content-disposition for File with name (#1133)event.req is instance of request (#1125)readMultipartFormData for backward compatibility (#1120)H3Core (#1127)req.headers (#1130)H3Event.app (#1139)fetch + event.app.fetch (#1141)content-disposition for File with name (#1133)event.req is instance of request (#1125)toResponse util (febb832)⚡ H3 v2 beta is here — fully rewritten on web standards, backward-compatible, and faster than ever!
⚡ H3 v2 beta is here — fully rewritten on web standards, backward-compatible, and faster than ever!
👉 Please check h3.dev/blog/v2-beta for release notes!
@aaharu, @AndreyYolkin, @BobbieGoede, @brc, @chadxz, @cjpearson, @danielrentz, @danielroe, @gulshan, @HigherOrderLogic, @hrynevychroman, @huseeiin, @iiio2, @Ingramz, @jerelmiller, @JianJroh, @jordypereira, @kanonji, @kricsleo, @kspace, @lborgav, @markthree, @maximepvrt, @Maxttier, @Narixius, @OmarMAttia7, @patak, @pi0, @productdevbook, @sandros94, @Shhu, @teleskop150750, @typed, @XiNiHa, @Youhan, @ysknsid25,
compare changes 🏡 Chore Update defu to 6.1.6 ( 6125485 ) Update deps ( 4998dd8 ) Update cookie-es
Preserve percent-encoded req.url in app event handler
static: Prevent path traversal via double-encoded dot segments (%252e%252e)
compare changes ### 🩹 Fixes - Preserve %25 in pathname
static: Narrow path traversal check to match .. as a path segment only
sse: Sanitize newlines in event stream fields to prevent SSE injection
> Security: Fixed a bug in readBody(event) and readRawBody(event) utils where certain Transfer-Encoding header formats could cause the request body to
[!IMPORTANT] Security: Fixed a bug in
readBody(event)andreadRawBody(event)utils where certainTransfer-Encodingheader formats could cause the request body to be ignored.In some deployments (for example, behind TCP load balancers or non-normalizing proxies), this could allow request smuggling. The handling is now safe and fully compliant. (read more)
Transfer-Encoding check causing request smuggling risk (618ccf4)getRequestHost: Return first host from x-forwarded-host
serveStatic: Omit decoded id from statusMessage
Handle FormData body (3757072 f38dd03 0c9b276)
setCookie: Properly merge and dedup set-cookie header
Migrate from unenv v1 to node-mock-http
unenv v1 to node-mock-http (#970)session: Allow using with crossws hooks
proxy: Ignore incoming accept-encoding header
readRawBody: Handle URLSearchParams
URLSearchParams (#888)getRequestUrl: Forward options to internal getRequestProtocol
getRequestProtocol (#776)onBeforeResponse and onAfterResponse are called with error code (#756)onClosed from firing twice in EventStream (#704)getRequestIP return value (#726)handleCors (#747)combined log format (#771)respondWith event object (#775)async for request body (#777)createError jsdoc (#762)getRequestProtocol (#776)onBeforeResponse and onAfterResponse are called with error code (#756)onClosed from firing twice in EventStream (#704)handleCors (#747)text/html content-type (#764)combined log format (#771)respondWith event object (#775)async for request body (#777)createError jsdoc (#762)ws: Resolve pathname for matching
H3 now has a new documentation website
sendIterable util (#655)etag header is set before sending 304 response (#653)sendIterable util (#655)etag header is set before sending 304 response (#653)console.log (#675)Your coding agent can read these notes before it upgrades. Set up the MCP server →