diff --git a/.changeset/access-log-host-count.md b/.changeset/access-log-host-count.md deleted file mode 100644 index 20c6f24..0000000 --- a/.changeset/access-log-host-count.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@ngrok/app-server": patch ---- - -The access log costs less on Node.js and Vercel. The listener counts the body bytes as it gives them to the socket, so the access log no longer puts a second stream around each body or copies the response. On Bun and Deno, the server counts the bytes with one stream around the body, as before. - -The line changes in these ways: - -- The line is for the response that leaves the server, after each stage. A response that an earlier plugin puts in place of the logged one now gets its line, with the status and bytes that the client got. Before, such a response got no line. -- `http.status_code` is the status that went out, such as the 500 for a response that could not go out. -- On Node.js and Vercel, `duration` ends when the response closes, after the last bytes go to the socket. For a slow client, it now includes the time to send the body. -- On Node.js and Vercel, `http.client_aborted` is also set when the client leaves before Node.js sends the last bytes, and when a body fails or ends before its `Content-Length` after the head. -- A log sink that throws no longer changes the response. Before, such a sink turned a HEAD request into a 500. diff --git a/.changeset/adversarial-review-fixes.md b/.changeset/adversarial-review-fixes.md deleted file mode 100644 index 5b97556..0000000 --- a/.changeset/adversarial-review-fixes.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@ngrok/app-server": patch ---- - -Fix a crash, a file leak, and a build exposure, and make each request cheaper. - -- On Node.js 26, a static file response that a later stage dropped unread, such as when a plugin threw after `next()`, crashed the process at garbage collection. The node host now opens a file at the first read, and the gzip stream, the access log, and the `runtimeEnv()` script injection read only when the client reads. -- A static file response never sends more bytes than its `Content-Length`, even when the file grows after the lookup. -- `nodeFiles().stat()` throws a file system error that does not mean a missing file, such as `EACCES` or `ELOOP`, so the request gets a logged 500, not a silent miss. `nodeFiles().open()` throws for a file that it cannot read, so the request gets a logged 500, not a 200 whose body fails. -- `streamingCompress()` sends a body whose `Content-Length` is under 1,024 bytes as it is. Gzip made such a body larger. It adds `Accept-Encoding` to `Vary` once, and only on a type that it can gzip, so an image no longer splits a CDN cache. -- The static stages hash the `etagPart` values once, not for each static file request. -- The `appServer` Vite plugin fails the build when the server build is inside the client build. Development does not check, because it serves no build directory. Before, the static stages could serve the server's code, or the whole file system when both builds used one directory. -- The `appServer` Vite plugin records the captured headers of a prerendered path with a non-ASCII character, such as `/café.json`, under the decoded path that the server looks up. -- The stale-build client no longer fails a request when `onNewBuild` throws: it reports the error. It reloads a document that the back/forward cache shows again after a move to a new build, and it does nothing on the server. -- `app-server` accepts `-p` for `--port` and `--help`. A bad argument writes a message and the usage, not a stack trace. diff --git a/.changeset/assets-in-memory.md b/.changeset/assets-in-memory.md deleted file mode 100644 index 005a3cc..0000000 --- a/.changeset/assets-in-memory.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@ngrok/app-server": patch ---- - -In production, the server keeps each static file up to 256 kibibytes (KiB) in memory after the first read, up to 32 mebibytes (MiB) in all. A later request or byte range needs no file read. A larger file streams from disk as before. On Node.js, `/robots.txt` answers about 50% more requests each second, and a small hashed asset about 55% more. diff --git a/.changeset/body-limit.md b/.changeset/body-limit.md deleted file mode 100644 index 522c09e..0000000 --- a/.changeset/body-limit.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@ngrok/app-server": minor ---- - -Add the `bodyLimit({ maxBytes })` plugin and the `bodyLimit` option of `defaultPlugins`. If a request body passes the limit, the client gets a 413. The body still streams to the app, and the plugin does not read it into memory. A `maxBytes` that is not a whole number of 0 or more stops startup. The "Limits" README section says what happens when the app streams the body into its response. - -Breaking change: with `defaultPlugins()`, a request body over 10 MiB now gets a 413. Before, the server had no limit. Set `defaultPlugins({ bodyLimit: 50 * 1024 * 1024 })` for a larger limit, or `bodyLimit: false` to keep the old behavior. diff --git a/.changeset/brotli-copies.md b/.changeset/brotli-copies.md deleted file mode 100644 index 9e37540..0000000 --- a/.changeset/brotli-copies.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@ngrok/app-server": minor ---- - -The `appServer` Vite plugin now also writes a brotli copy (`..br`) next to each gzip copy, when the brotli copy is smaller. `app-server serve` sends the copy that the client's `Accept-Encoding` prefers: brotli, then gzip, then the file. It follows the `q` values, and `q=0` refuses an encoding. In the build of a real docs site, the gzip copies of the 337 scripts are 2.57 megabytes (MB) in all, and the brotli copies are 2.07 MB. - -Each encoding of a static file now has its own ETag, such as `W/"5fa9c-19a4e2c8f00-br"`. So a cache that keeps more than one encoding of a file picks the correct one after a 304. A client that holds the ETag of a gzip copy from an earlier version gets one 200 with the new ETag. - -`streamingCompress()` now gzips for `Accept-Encoding: *`. It sends the body as it is when `identity` has a higher `q` than gzip. - -If you keep the `/assets/*` files of old builds for open tabs, also keep their `..br` copies. diff --git a/.changeset/build-id-from-build.md b/.changeset/build-id-from-build.md deleted file mode 100644 index ce7b77c..0000000 --- a/.changeset/build-id-from-build.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@ngrok/app-server": patch ---- - -`buildId()` needs no `value` now. After the client build, the `appServer` Vite plugin hashes the name and the content of each file of the client bundle. After the server build, it adds each file that the server build wrote to the client's assets directory, such as the React Router route manifest. It writes the hash as `buildId` in `.app-server.json`. With no `value`, `buildId()` sends that ID. - -- The ID changes when the code or the routes that an open tab runs change. A deploy that changes only the server, a `public/` file, or a prerendered page keeps it, so an open tab does not move for nothing. -- An explicit `value`, such as the image tag, wins. With an unset or empty variable, such as `value: process.env.IMAGE_TAG`, the plugin sends the build's ID. -- In development, `buildId()` with no `value` sends no build, because Vite's HMR updates an open tab after each edit. - -Build the app again, so `.app-server.json` has the build ID. diff --git a/.changeset/byte-ranges.md b/.changeset/byte-ranges.md deleted file mode 100644 index 76ae019..0000000 --- a/.changeset/byte-ranges.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@ngrok/app-server": minor ---- - -Answer a `Range` request for a static file. A GET with one `bytes` range gets a 206, and a range past the end of the file gets a 416. Safari now plays a `