mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix: don't call Bun.serve() on exported Server instances (#26144)
## Summary
- Fixes the entry point wrapper to distinguish between Server
configuration objects and already-running Server instances
- When a Server object from `Bun.serve()` is exported as the default
export, Bun no longer tries to call `Bun.serve()` on it again
## Root Cause
The entry point wrapper in `src/bundler/entry_points.zig` checks if the
default export has a `fetch` method to auto-start servers:
```javascript
if (typeof entryNamespace?.default?.fetch === 'function' || ...) {
const server = Bun.serve(entryNamespace.default);
}
```
However, `Server` objects returned from `Bun.serve()` also have a
`fetch` method (for programmatic request handling), so the wrapper
mistakenly tried to call `Bun.serve(server)` on an already-running
server.
## Solution
Added an `isServerConfig()` helper that checks:
1. The object has a `fetch` function or `app` property (config object
indicators)
2. The object does NOT have a `stop` method (Server instance indicator)
Server instances have `stop`, `reload`, `upgrade`, etc. methods, while
config objects don't.
## Test plan
- [x] Added regression test that verifies exporting a Server as default
export works without errors
- [x] Added test that verifies config objects with `fetch` still trigger
auto-start
- [x] Verified test fails with `USE_SYSTEM_BUN=1` and passes with the
fix
Fixes #26142
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
96
test/regression/issue/26142.test.ts
Normal file
96
test/regression/issue/26142.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26142
|
||||
// When exporting a Server object from Bun.serve() as the default export,
|
||||
// Bun's entry point wrapper should not try to call Bun.serve() on it again.
|
||||
|
||||
test("exporting server as default export should not error", async () => {
|
||||
using dir = tempDir("issue-26142", {
|
||||
"server.js": `
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { GET: () => Response.json({ message: "Hello" }) },
|
||||
},
|
||||
fetch(req) {
|
||||
return Response.json({ error: "Not Found" }, { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server running on port " + server.port);
|
||||
|
||||
// Stop the server immediately so the process can exit
|
||||
server.stop();
|
||||
|
||||
// This export was causing the issue - entry point wrapper would try to
|
||||
// call Bun.serve() on the already-running server
|
||||
export default server;
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should not have any errors related to double-serving
|
||||
expect(stderr).not.toContain("EADDRINUSE");
|
||||
expect(stderr).not.toContain("Maximum call stack");
|
||||
expect(stderr).not.toContain("is already listening");
|
||||
|
||||
// Check stdout for the expected message
|
||||
expect(stdout).toContain("Server running on port");
|
||||
|
||||
// Process should exit successfully
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("server config with fetch as default export should still auto-start", async () => {
|
||||
using dir = tempDir("issue-26142-config", {
|
||||
"server.js": `
|
||||
// Export a config object (not a server instance)
|
||||
// This should still trigger auto-start
|
||||
export default {
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
return Response.json({ working: true });
|
||||
},
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Set a timeout to kill the server after checking output
|
||||
const timeout = setTimeout(() => proc.kill(), 3000);
|
||||
|
||||
try {
|
||||
// Wait for first bit of stdout to verify server started
|
||||
const reader = proc.stdout.getReader();
|
||||
const { value } = await reader.read();
|
||||
reader.releaseLock();
|
||||
|
||||
// Decode the output
|
||||
const decoder = new TextDecoder();
|
||||
const output = decoder.decode(value);
|
||||
|
||||
// Should have started the server (look for the debug message on stdout)
|
||||
expect(output).toContain("Started");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
proc.kill();
|
||||
await proc.exited;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user