Commit activity
Last 12 weeks · 0 commits
Community health
2 of 6 standards met
Recent PRs & issues
Fixes #235. Summary Destroy the file read stream before returning 304 Not Modified. Add a regression that is destroyed on an hit. Why The handler opened the file before evaluating the conditional request. On 304 the stream was never piped, so each cache hit leaked an FD. Under normal browser traffic that is most requests, until . Validation (71 passing)
Fixes #205. Files with non-ASCII names such as or fail with and the request never gets a valid response. Problem keeps ISO-8859-1 characters in the plain parameter. Node.js rejects the header when the response also sets , so throws for the header. Changes Passes an ASCII-only to . Non-ASCII characters in the plain parameter become , and the full name is sent in (RFC 5987). ASCII names produce the same header as before. Adds an integration test that serves files named with Latin-1, a middle dot, and CJK characters. Testing Before the change, the new test fails with status 500 and the other 70 tests pass. After the change, all 71 tests pass and is clean.
Summary When responds with , the read stream that was opened earlier in the request handler is never consumed or destroyed. Because the file descriptor (FD) held by that stream is only released when the stream is piped (200) or explicitly destroyed, every response leaks exactly one FD. Under normal browser traffic (most requests are conditional requests that resolve to ), FDs accumulate until the process hits the OS limit and throws , crashing the server. Affected version Also present on the current branch (the 304 branch does not destroy the stream). Root cause In , the handler opens the stream before evaluating the conditional request: On the path the stream is piped to the response, so its FD is released when the transfer finishes. On the path the function returns without piping or destroying the stream, so the underlying FD stays open forever. Impact Every response leaks 1 FD. FDs never decrease while the process is alive (verified: FD count stayed flat for hours under no load, and kept climbing under load until it reached the FD limit). Once the limit is reached, subsequent calls fail with , crashing the process. In containerized deployments this manifests as intermittent 5xx errors and restarts. Observed error: Steps to reproduce 1. Serve any static directory with / . 2. Request a cacheable asset repeatedly so the client sends and the server responds (e.g. reload a page many times). 3. Observe the process FD count (e.g. ) grow by ~1 per and never decrease. 4. Continue until the FD limit is reached; the process throws and crashes. Proposed fix Destroy the opened stream before returning on the branch, so its FD is released: I'm happy to open a PR with this change if it's acceptable. diff diff --git a/src/index.js b/src/index.js index 564f012..ed1c5b2 100644 a/src/index.js +++ b/src/index.js @@ -759,6 +759,7 @@ module.exports = async (request, response, config = {}, methods = {}) => { // eslint-disable-next-line no-eq-null if (request.headers.range == null && headers.ETag && headers.ETag === request.headers['if-none-match']) { response.statusCode = 304; + stream.destroy(); response.end(); return; ```
Recent fixes
What documents that symlinks are disabled by default and are treated as if they do not exist (README, ). The check only inspected the final path component, so two ordinary requests were still followed and could serve files outside the directory: a directory symlink requested with a trailing slash () — resolves the final symlink when the path ends with a symlink used as an intermediate path component () — leaves only the final component unresolved This defeats the documented default, which exists specifically to keep symlinks from escaping the served root. Fix walks the path components with and returns a 404 as soon as one of them is a symlink, which is what the documented default promises. The final component continues to be checked by the existing code path, except when the request has a trailing slash, where the new walk covers it. Tests (no regression) Full suite: . Notes Behaviour with is unchanged. Unrelated to this change: with , requesting a directory symlink without a trailing slash currently throws an uncaught from and terminates the process, because is not re-evaluated after . I can send a follow-up patch for that if it is useful.
Repository: vercel/serve-handler. Description: The foundation of `serve` Stars: 617, Forks: 119. Primary language: JavaScript. Languages: JavaScript (88.2%), EJS (11.8%). License: MIT. Homepage: https://npmjs.com/serve-handler Topics: handler, middleware, server, single-page-applications, static. Latest release: 6.1.7 (7mo ago). Open PRs: 40, open issues: 49. Last activity: 5mo ago. Community health: 50%. Top contributors: leo, AndyBitz, TooTallNate, dependabot[bot], thasophearak, wellingguzman, kachkaev, andyburke, AxlLind, balupton and others.