-
Notifications
You must be signed in to change notification settings - Fork 5k
Bun.serve: cancel the piped upstream fetch when the client aborts while backpressured #36700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
robobun
wants to merge
3
commits into
main
Choose a base branch
from
farm/7cc528e9/serve-fetch-abort-followup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+122
−1
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
11 changes: 11 additions & 0 deletions
11
test/js/bun/http/serve-fetch-body-abort-backpressure-fixture.ts
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
102 changes: 102 additions & 0 deletions
102
test/js/bun/http/serve-fetch-body-abort-backpressure.test.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe } from "harness"; | ||
| import net from "node:net"; | ||
| import { join } from "node:path"; | ||
|
|
||
| // A client that aborts while Bun.serve is holding an upstream fetch() body | ||
| // paused for backpressure must not leave the upstream parked: on_abort has | ||
| // to cancel the ByteStream sink so the proxy tears down its connection to | ||
| // the upstream. | ||
| test("client abort while a fetch() body is backpressured cancels the upstream", async () => { | ||
| const CHUNK = 256 * 1024; | ||
| const CAP_CHUNKS = 256; | ||
|
|
||
| let pulls = 0; | ||
| let cancelled = false; | ||
|
|
||
| await using upstream = Bun.serve({ | ||
| port: 0, | ||
| idleTimeout: 255, | ||
| fetch() { | ||
| return new Response( | ||
| new ReadableStream({ | ||
| async pull(controller) { | ||
| controller.enqueue(new Uint8Array(CHUNK)); | ||
| pulls++; | ||
| if (pulls >= CAP_CHUNKS) return controller.close(); | ||
| if (pulls % 32 === 0) await Bun.sleep(0); | ||
| }, | ||
| cancel() { | ||
| cancelled = true; | ||
| }, | ||
| }), | ||
| { headers: { "content-length": String(CHUNK * CAP_CHUNKS) } }, | ||
| ); | ||
| }, | ||
| }); | ||
|
|
||
| await using proxy = Bun.spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| join(import.meta.dir, "serve-fetch-body-abort-backpressure-fixture.ts"), | ||
| `http://127.0.0.1:${upstream.port}/`, | ||
| ], | ||
| env: bunEnv, | ||
| stdout: "pipe", | ||
| stderr: "inherit", | ||
| }); | ||
|
|
||
| const reader = proxy.stdout.getReader(); | ||
| let head = ""; | ||
| while (!head.includes("\n")) { | ||
| const { value, done } = await reader.read(); | ||
| if (done) throw new Error("proxy exited before reporting port"); | ||
| head += Buffer.from(value).toString("utf8"); | ||
| } | ||
| reader.releaseLock(); | ||
| const { proxyPort } = JSON.parse(head.slice(0, head.indexOf("\n"))); | ||
|
|
||
| const failed = Promise.withResolvers<never>(); | ||
| proxy.exited.then(code => failed.reject(new Error(`proxy exited early (code ${code})`))); | ||
|
|
||
| const socket = net.connect(proxyPort, "127.0.0.1"); | ||
| try { | ||
| const stalled = Promise.withResolvers<void>(); | ||
| socket.on("error", e => failed.reject(e)); | ||
| socket.on("connect", () => socket.write("GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")); | ||
| socket.once("data", () => { | ||
| socket.pause(); | ||
| stalled.resolve(); | ||
| }); | ||
| await Promise.race([stalled.promise, failed.promise]); | ||
|
|
||
| // Wait until the upstream pull count stops growing (backpressure engaged) | ||
| // or the upstream produces the whole capped body (no backpressure at all). | ||
| let lastPulls = -1; | ||
| let stableTurns = 0; | ||
| while (pulls < CAP_CHUNKS && stableTurns < 12) { | ||
| await Bun.sleep(25); | ||
| if (pulls === lastPulls) stableTurns++; | ||
| else { | ||
| stableTurns = 0; | ||
| lastPulls = pulls; | ||
| } | ||
| } | ||
| expect(pulls).toBeLessThan(CAP_CHUNKS / 2); | ||
| } finally { | ||
| socket.removeAllListeners("error"); | ||
| socket.on("error", () => {}); | ||
| socket.destroy(); | ||
| } | ||
|
|
||
| // The proxy's on_abort should cancel its fetch to the upstream; the | ||
| // upstream's serve then cancels the ReadableStream. Poll for that signal. | ||
| for (let i = 0; i < 200 && !cancelled; i++) await Bun.sleep(10); | ||
|
|
||
| expect({ cancelled, pullsUnderCap: pulls < CAP_CHUNKS, pulls }).toMatchObject({ | ||
| cancelled: true, | ||
| pullsUnderCap: true, | ||
| }); | ||
|
robobun marked this conversation as resolved.
|
||
| expect(proxy.exitCode).toBeNull(); | ||
| expect(proxy.signalCode).toBeNull(); | ||
| }); | ||
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.
Uh oh!
There was an error while loading. Please reload this page.