Repository navigation
v6.0.0: fix functional bugs found by adversarial review, 100% test coverage - #105
Merged
Merged
Conversation
Regression tests for bugs confirmed by an adversarial review panel: routing/url rewriting, query strings, timeouts, error hooks, route config mutation, default http/lambda response hooks, hostnames hook and the WebSocket proxy. The large chunked "Connection: close" smoke test now expects the response to be streamed instead of rejected with 502. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
Routing (index.js): - onError hook sending a response no longer triggers a second response (ERR_HTTP_HEADERS_SENT crash) - async proxyHandler rejections reach onError/next instead of crashing - reject "."/".." path segments (incl. %2e and backslash forms) with 400, they let requests escape the route prefix upstream - repeated query parameters are forwarded as-is instead of "a=1%2C2" - req.query is writable in onRequest when there is no query string - prefix is stripped case-insensitively, only at the start of the url and prefixRewrite is used literally - prefix: '/' (or a trailing slash prefix) works as a catch-all - hooks.request no longer drops the route timeout, timeout: 0 disables it - route and hooks configs are not mutated (shared/reused configs broke) - accept the documented "http-legacy" proxy type - services.json prints RegExp prefixes Default hooks: - lambda: default status code, serialize non-string bodies, decode isBase64Encoded bodies (missing statusCode crashed the process) - http (Connection: close): stream responses above the 1MB buffer limit instead of failing with 502, release the upstream when the client goes away, drop upstream headers on error responses Hostnames hook: case-insensitive matching, no unbounded per-host cache, no prototype key collisions. WebSocket proxy: explicit relay propagating close codes and closing the peer on errors, queue messages during async onOpen, don't connect upstream for clients that already left, sanitize onOpen close codes, don't hang non-WebSocket upgrades or end sockets owned by other upgrade listeners, forward only the negotiated subprotocol and the original query string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
- keep req.query as a plain object for non "http" proxy types, fast-proxy
(http-legacy) stringified a URLSearchParams into an empty query
- avoid a double slash when prefixRewrite ends with "/"
- destroy a partially sent response when the handler fails without an
onError hook instead of leaving the client hanging
- also reject dot segments followed by ";" path parameters ("/..;/")
- bound the messages queued while an async WebSocket onOpen hook is
pending (1MB, closes the client with 4413)
- don't write hooks or matchers into WebSocket route configs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
- destroy a partially sent response when onError doesn't respond either, instead of leaving the client hanging - reject dot segments separated by encoded slashes/backslashes (%2f, %5c) - don't append "?" to upstream urls when there is no query string - keep proxyOpts.queryString a plain object; repeated parameters are appended individually through an iterator of [name, value] pairs, which also works for fast-proxy (http-legacy) and custom proxy types - move the http-legacy default hooks to lib/default-hooks.js - don't crash on a non-string pathRegex with a trailing slash prefix - WebSocket: report upstream closes that can't be relayed (e.g. 1006, 1011) as abnormal closes to the client instead of 1000 - WebSocket: truncate close reasons once, on a UTF-8 character boundary - WebSocket: end upgrades no listener handled within 1s when the server has other 'upgrade' listeners (same approach as engine.io) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
- cover proxy factory fallbacks, custom and unsupported proxy types, http-legacy (with a fast-proxy stand-in), websocket routes on servers without getServer(), unsupported methods, non literal prefixes, servers without req.query, onError completing partial responses, lambda payloads without status code/body and hostnames without host header - fix: a "Connection: close" buffered response for an upstream using "Transfer-Encoding: gzip, chunked" was sent with both transfer-encoding and content-length (invalid framing) - remove unreachable code: micromatch fallback in the hostnames hook, 1005 close code mapping (websocket-driver reports 1000), pause/drain relay handling (faye send() only returns false once closing), empty pathname fallback and the error listener added before destroy() - enforce 99% statements/branches/functions/lines coverage with nyc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134bwfxe5igRZeU6cwBSfzE
- restana ^7.0.0: routes are matched case-sensitively by default
(restana: { caseSensitive: false } restores the previous behavior);
inline the HTTP methods list, restana/libs/methods is no longer exported
- mocha ^12.0.3, @types/node ^26.6.5, lockfile refreshed
- require Node.js >= 24 (engines field, CI on 24.x)
- docs: v6.x breaking changes, Node.js requirement, pinned docsify assets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A panel of adversarial reviewers looked at four areas: routing, response hooks, the WebSocket proxy, and hostnames/config. Each bug needed a script that reproduced it before it counted. The fixes were then made test-first: the failing regression tests went in first (RED), then the fixes (GREEN). Two more adversarial review rounds of the branch followed. Every finding from those rounds was either fixed with its own test or is explained under Notes.
The PR also upgrades to restana 7 and moves every dependency to its latest version. restana 7 requires Node.js 24 and matches routes case-sensitively.
The suite grows from 57 to 131 tests and coverage goes from about 90% to 100% (statements, branches, functions, lines). nyc now fails CI below 99%. The version moves to 6.0.0 (latest on npm is 5.0.1) because several of these changes affect behavior someone may depend on (see Breaking changes).
Breaking changes
package.jsonnow declaresengines, and CI runs on Node 24.x.GET /Users/42no longer matchesprefix: '/users'. With a catch-all route it goes there, unchanged.restana: { caseSensitive: false }restores the previous behavior.errorHandlerrespond with the standard status text, e.g.{"code":404,"message":"Not Found"}instead of"Internal Server Error"..or..segments now get 400. This includes%2eforms,\,%2fand%5cseparators, and;parameters such as/..;/. Before, they got past the route's prefix and the middlewares of the route they were meant to hit, because the upstream resolved them later.caseSensitive: false.prefixRewriteis used literally, so$&and other replacement patterns are no longer expanded.prefix: '/'now works as a catch-all, and prefixes ending in/match nested paths.prefixRewriteending in/no longer produces//.Connection: closeresponses: chunked responses over 1MB are now streamed instead of rejected with 502. Clients that always sendConnection: close, such as Python's urllib, couldn't download them before. Memory is still capped at 1MB.timeout: 0on a route now disables the global timeout, as documented.hooks.requestis merged with the timeout instead of replacing it.req.timeouttakes precedence overhooks.request.timeout.?a=1&a=2) are forwarded as-is; before, they arrived asa=1%2C2.?is appended when there is no query string. In that caseproxyOpts.queryStringisnull, as it already was withdisableQsOverwrite.gateway()(HTTP and WebSocket) are no longer modified. Sharing a hooks object between an http route and a lambda route, or reusing a routes array, used to crash or leak settings into the second gateway.onError: if the hook sends a response,next(err)is skipped; before, this crashed the process withERR_HTTP_HEADERS_SENT. A response that was already partly sent when the error happened is destroyed instead of left hanging.hostname2prefixarray are not picked up.micromatchis now required; it was already a dependency.onOpenis running are queued and forwarded. This includes any message the hook itself reads. Past 1MB of queued data, the client is closed with 4413.'upgrade'listener. When other listeners exist, the socket is ended after 1s if none of them answered. This is the same approach engine.io uses.Bug fixes
Crashes and hangs
asyncproxyHandlerthat rejected caused an unhandled rejection or a request that never finished. It now goes toonErrorandnext(err).statusCode, or with an objectbody, crashed the process.onOpenerror with close code 1008/1011, or a rejection with no value, crashed the process. A hook that threw synchronously lost its close code. An unreachable or reset upstream left the client open forever. When the client went away, the upstream connection leaked. Upgrades that weren't WebSocket hung.pathRegex: undefinedwith a prefix ending in/threw at startup.Wrong responses
isBase64Encodedis honored, non-string bodies are serialized, and the status code defaults to 200.Connection: close: the upstream connection is released when the client disconnects while buffering. Error responses no longer carry upstream headers such ascontent-encoding: gzip. ATransfer-Encoding: gzip, chunkedupstream is no longer answered with bothtransfer-encodingandcontent-length.req.query.x = …inonRequestfailed when the request had no query string: restana uses a frozen empty object there.proxyOpts.queryStringis always a plain object. Repeated parameters are serialized correctly for fast-proxy-lite, fast-proxy (http-legacy) and custom proxies.http-legacyproxy type, listed in the docs, was rejected at startup./services.jsonprinted{}for RegExp prefixes.onOpenwere lost. A bogus handshake, or a client that left duringonOpen, still opened an upstream connection. Close reasons are now cut on a UTF-8 character boundary.Resource exhaustion
constructor) also produced broken URLs.Dependencies
restana^6.0.0 → ^7.0.0. restana 7 no longer exportsrestana/libs/methods, so the gateway's list of HTTP methods is now defined inline.mocha^11.7.5 → ^12.0.3,@types/node^25.7.0 → ^26.6.5.npm audit --omit=dev: 0 vulnerabilities. The 7 remaining findings are in dev-only tools (artillery, aws-sdk via http-lambda-proxy, @azure/identity) and have no non-breaking fix.Docs
docs/README.md): new v6.x breaking-changes section and a note that Node.js 24 is required. The comment on therestanaoption explainscaseSensitive.docs/index.html: the docsify assets load exact versions, and a stray.rendered on the page is gone.README.md: the Node.js requirement and a link to the breaking changes.Tests
test/routing.test.js,test/default-hooks.test.js,test/ws-proxy-bugs.test.js, and a raw HTTP client helper,test/raw-http.js.test/routing.test.jscovers both case-sensitive routing (the default) andcaseSensitive: false.test/hostnames-hook.test.js.test/smoke.test.js: the large-chunkedConnection: closetest now expects the response to be streamed.check-coveragethresholds of 99% are set inpackage.json.Notes
package-lock.jsonwas out of sync withpackage.jsononmaster, sonpm cifailed there. It has been regenerated andnpm ciworks again.target(e.g.http://host/v2) is still dropped. That is how fast-proxy-lite builds URLs.urlRewriteis still replaced byreq.query;disableQsOverwriteis the documented way to keep it.onError, because the library answers them itself.Test plan
npm test(Node 26): 131 passing, 100% coverage on statements, branches, functions and linesnpx standard: cleannpm ci --dry-run: lockfile in sync🤖 Generated with Claude Code