From e9749cefa56946ac46c66bf191ca9bd34d6cb004 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 28 May 2025 22:30:50 -0700 Subject: [PATCH] test: add http server consumed timeout --- src/bun.js/api/server.zig | 14 +++- .../test-http-server-consumed-timeout.js | 84 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 test/js/node/test/parallel/test-http-server-consumed-timeout.js diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index b47b5380c3..a46d64157c 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2292,6 +2292,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp flags: NewFlags(debug_mode) = .{}, + /// timeout in seconds set via IncomingMessage.setTimeout(). + request_timeout_seconds: u8 = 0, + upgrade_context: ?*uws.uws_socket_context_t = null, /// We can only safely free once the request body promise is finalized @@ -4477,6 +4480,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp assert(this.resp == resp); + if (this.request_timeout_seconds > 0) { + resp.timeout(this.request_timeout_seconds); + } + this.flags.is_waiting_for_request_body = last == false; if (this.isAbortedOrEnded() or this.flags.has_marked_complete) return; if (!last and chunk.len == 0) { @@ -4647,8 +4654,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp pub fn setTimeout(this: *RequestContext, seconds: c_uint) bool { if (this.resp) |resp| { - resp.timeout(@min(seconds, 255)); - if (seconds > 0) { + const secs: u8 = @truncate(@min(seconds, 255)); + this.request_timeout_seconds = secs; + resp.timeout(secs); + if (secs > 0) { // we only set the timeout callback if we wanna the timeout event to be triggered // the connection will be closed so the abort handler will be called after the timeout @@ -4659,6 +4668,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } } else { // if the timeout is 0, we don't need to trigger the timeout event + this.request_timeout_seconds = 0; resp.clearTimeout(); } return true; diff --git a/test/js/node/test/parallel/test-http-server-consumed-timeout.js b/test/js/node/test/parallel/test-http-server-consumed-timeout.js new file mode 100644 index 0000000000..1d7967bbe0 --- /dev/null +++ b/test/js/node/test/parallel/test-http-server-consumed-timeout.js @@ -0,0 +1,84 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const durationBetweenIntervals = []; +let timeoutTooShort = false; +const TIMEOUT = common.platformTimeout(200); +const INTERVAL = Math.floor(TIMEOUT / 8); + +runTest(TIMEOUT); + +function runTest(timeoutDuration) { + let intervalWasInvoked = false; + let newTimeoutDuration = 0; + const closeCallback = (err) => { + assert.ifError(err); + if (newTimeoutDuration) { + runTest(newTimeoutDuration); + } + }; + + const server = http.createServer((req, res) => { + server.close(common.mustCall(closeCallback)); + + res.writeHead(200); + res.flushHeaders(); + + req.setTimeout(timeoutDuration, () => { + if (!intervalWasInvoked) { + // Interval wasn't invoked, probably because the machine is busy with + // other things. Try again with a longer timeout. + newTimeoutDuration = timeoutDuration * 2; + console.error('The interval was not invoked.'); + console.error(`Trying w/ timeout of ${newTimeoutDuration}.`); + return; + } + + if (timeoutTooShort) { + intervalWasInvoked = false; + timeoutTooShort = false; + newTimeoutDuration = + Math.max(...durationBetweenIntervals, timeoutDuration) * 2; + console.error(`Time between intervals: ${durationBetweenIntervals}`); + console.error(`Trying w/ timeout of ${newTimeoutDuration}`); + return; + } + + assert.fail('Request timeout should not fire'); + }); + + req.resume(); + req.once('end', () => { + res.end(); + }); + }); + + server.listen(0, common.mustCall(() => { + const req = http.request({ + port: server.address().port, + method: 'POST' + }, () => { + let lastIntervalTimestamp = Date.now(); + const interval = setInterval(() => { + const lastDuration = Date.now() - lastIntervalTimestamp; + durationBetweenIntervals.push(lastDuration); + lastIntervalTimestamp = Date.now(); + if (lastDuration > timeoutDuration / 2) { + // The interval is supposed to be about 1/8 of the timeout duration. + // If it's running so infrequently that it's greater than 1/2 the + // timeout duration, then run the test again with a longer timeout. + timeoutTooShort = true; + } + intervalWasInvoked = true; + req.write('a'); + }, INTERVAL); + setTimeout(() => { + clearInterval(interval); + req.end(); + }, timeoutDuration); + }); + req.write('.'); + })); +}