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:
robobun
2026-01-15 20:28:32 -08:00
committed by GitHub
parent 12243b9715
commit 5b25a3abdb
2 changed files with 106 additions and 4 deletions

View 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;
}
});