Compare commits

...

4 Commits

Author SHA1 Message Date
autofix-ci[bot]
076b20083a [autofix.ci] apply automated fixes 2026-01-16 07:42:58 +00:00
Jarred Sumner
d0cc8986e9 Merge branch 'main' into claude/fix-server-response-writable-ended-25632 2026-01-15 23:41:16 -08:00
Jarred Sumner
63435e5176 Merge branch 'main' into claude/fix-server-response-writable-ended-25632 2026-01-14 13:38:15 -08:00
Claude Bot
e6d9cdf299 fix(http): set finished to true in ServerResponse.end() when no handle
When `ServerResponse.end()` is called without an underlying socket/handle
(e.g., when using `light-my-request` for testing), the early-return path
was not setting `this.finished = true`, causing `writableEnded` to
incorrectly return `false`. This broke Fastify compatibility with
light-my-request for testing scenarios.

The fix adds the same lifecycle handling in the no-handle code path:
- Sets `this.finished = true`
- Emits "prefinish" and "finish" events
- Calls the callback if provided
- Emits "close" event via nextTick

Fixes #25632

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 20:08:37 +00:00
3 changed files with 75 additions and 3 deletions

View File

@@ -1089,8 +1089,8 @@ const string = []const u8;
const Environment = @import("../../env.zig");
const std = @import("std");
const FetchRedirect = @import("../../http/FetchRedirect.zig").FetchRedirect;
const FetchCacheMode = @import("../../http/FetchCacheMode.zig").FetchCacheMode;
const FetchRedirect = @import("../../http/FetchRedirect.zig").FetchRedirect;
const FetchRequestMode = @import("../../http/FetchRequestMode.zig").FetchRequestMode;
const Method = @import("../../http/Method.zig").Method;

View File

@@ -1335,8 +1335,32 @@ ServerResponse.prototype.end = function (chunk, encoding, callback) {
}
if (!handle) {
if ($isCallable(callback)) {
process.nextTick(callback);
this.finished = true;
process.nextTick(self => {
self._ended = true;
}, this);
this.emit("prefinish");
this._callPendingCallbacks();
if (callback) {
process.nextTick(
function (callback, self) {
self.emit("finish");
try {
callback();
} catch (err) {
self.emit("error", err);
}
process.nextTick(emitCloseNT, self);
},
callback,
this,
);
} else {
process.nextTick(function (self) {
self.emit("finish");
process.nextTick(emitCloseNT, self);
}, this);
}
return this;
}

View File

@@ -0,0 +1,48 @@
import { expect, test } from "bun:test";
import http from "node:http";
test("ServerResponse.writableEnded should be true after end() when no socket", () => {
const req = new http.IncomingMessage(null);
const res = new http.ServerResponse(req);
expect(res.finished).toBe(false);
expect(res.writableEnded).toBe(false);
res.end();
expect(res.finished).toBe(true);
expect(res.writableEnded).toBe(true);
});
test("ServerResponse.writableEnded should be true after end() with callback when no socket", async () => {
const req = new http.IncomingMessage(null);
const res = new http.ServerResponse(req);
expect(res.finished).toBe(false);
expect(res.writableEnded).toBe(false);
let callbackCalled = false;
res.end(() => {
callbackCalled = true;
});
expect(res.finished).toBe(true);
expect(res.writableEnded).toBe(true);
// Wait for the callback to be called via process.nextTick
await new Promise(resolve => process.nextTick(resolve));
expect(callbackCalled).toBe(true);
});
test("ServerResponse.writableEnded should be true after end() with chunk when no socket", () => {
const req = new http.IncomingMessage(null);
const res = new http.ServerResponse(req);
expect(res.finished).toBe(false);
expect(res.writableEnded).toBe(false);
res.end("test");
expect(res.finished).toBe(true);
expect(res.writableEnded).toBe(true);
});