Skip to content

feat(node/http): enable keepAlive connection reuse for HTTP Agent#31709

Merged
fraidev merged 5 commits intodenoland:mainfrom
fraidev:faster_http
Dec 30, 2025
Merged

feat(node/http): enable keepAlive connection reuse for HTTP Agent#31709
fraidev merged 5 commits intodenoland:mainfrom
fraidev:faster_http

Conversation

@fraidev
Copy link
Copy Markdown
Contributor

@fraidev fraidev commented Dec 23, 2025

Closes #29676

Enables HTTP Agent keepAlive connection pooling for improved performance with Node.js HTTP clients.

Changes:

  • Use conn.without_shutdown() to allow socket reuse after HTTP response
  • Add op_node_http_response_reclaim_conn op to return socket to pool
  • Detect WebSocket upgrades via Connection: Upgrade header and use with_upgrades() for those requests
  • Reset socket stream state flags for reuse (kDestroyed, kErrored, etc.)
  • Map stale connection errors to ECONNRESET for client retry logic
  • Export kEnding/kEnded from internal/streams/utils.js

This significantly improves performance for HTTP clients using keepAlive, reducing request latency from ~500ms to ~150ms in real-world scenarios like AWS SDK requests.

Results of @littledivy's repro in my M1 Max MacBook Pro:

Node v25.2.1:

$ wrk http://localhost:8080                                                                                    
Running 10s test @ http://localhost:8080
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   157.53ms   52.33ms 519.49ms   96.87%
    Req/Sec    35.95     14.18    50.00     86.99%
  649 requests in 10.09s, 107.11KB read
Requests/sec:     64.29
Transfer/sec:     10.61KB

Deno 2.6.2 (without these changes):

$ wrk http://localhost:8080   
Running 10s test @ http://localhost:8080
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   626.12ms   20.26ms 699.27ms   88.12%
    Req/Sec    10.16      7.95    39.00     81.82%
  160 requests in 10.09s, 22.66KB read
Requests/sec:     15.86
Transfer/sec:      2.25KB

Deno with these changes:

$ wrk http://localhost:8080
Running 10s test @ http://localhost:8080
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   164.37ms   83.74ms 696.62ms   95.58%
    Req/Sec    36.96     13.40    50.00     94.81%
  649 requests in 10.10s, 91.90KB read
Requests/sec:     64.25
Transfer/sec:      9.10KB

@fraidev fraidev force-pushed the faster_http branch 2 times, most recently from b769190 to e6c6fc1 Compare December 23, 2025 21:03
@Hajime-san
Copy link
Copy Markdown
Contributor

Can we pass some node_compat tests in tests/node_compat/config.toml with this change?

https://github.com/search?q=repo%3Adenoland%2Fnode_test+agent&type=code

@Hajime-san
Copy link
Copy Markdown
Contributor

It would be great if we could confirm whether this repro is getting faster.

@fraidev
Copy link
Copy Markdown
Contributor Author

fraidev commented Dec 24, 2025

Can we pass some node_compat tests in tests/node_compat/config.toml with this change?

https://github.com/search?q=repo%3Adenoland%2Fnode_test+agent&type=code

Yeah, parallel/test-http-client-abort-keep-alive-destroy-res.js is passing now!

@fraidev
Copy link
Copy Markdown
Contributor Author

fraidev commented Dec 24, 2025

It would be great if we could confirm whether this repro is getting faster.

I couldn't replicate this, seems that is not slow now, even with the latest Deno.

@diffray-bot
Copy link
Copy Markdown

Changes Summary

This PR implements HTTP Agent keepAlive connection reuse for Node.js HTTP clients, enabling socket pooling and reuse across multiple requests. The changes reduce latency from ~626ms to ~164ms by avoiding new socket creation for each request, bringing Deno's performance in line with Node.js.

Type: feature

Components Affected: ext/node/ops/http.rs, ext/node/polyfills/http.ts, ext/node/polyfills/internal/streams/utils.js, Node.js HTTP Agent implementation, HTTP connection pooling

Files Changed
File Summary Change Impact
/tmp/workspace/ext/node/ops/http.rs Added socket reclamation mechanism with new op_node_http_response_reclaim_conn op and oneshot channel for returning sockets after HTTP responses; detects WebSocket upgrades and uses appropriate connection handling. ✏️ 🔴
/tmp/workspace/ext/node/polyfills/http.ts Implemented keepAlive socket reuse logic in IncomingMessageForClient with #tryReturnSocket method, socket state reset, and agent pool integration; added error handling for stale connections. ✏️ 🔴
...ce/ext/node/polyfills/internal/streams/utils.js Exported kEnding and kEnded socket state flags needed for resetting socket stream state during keepAlive reuse. ✏️ 🟢
.../specs/node/http_agent_keepalive/__test__.jsonc New spec test verifying HTTP Agent keepAlive connection reuse across multiple requests. 🟡
...e/tests/specs/node/http_agent_keepalive/main.ts Integration test that validates socket reuse in freeSockets pool and reusedSocket flag across three sequential requests. 🟡
/tmp/workspace/tests/unit_node/http_test.ts Updated test structure to use sanitizeResources: false for async HTTPS tests to prevent resource cleanup issues. ✏️ 🟡
...space/tests/unit_node/http_no_cert_flag_test.ts Updated test structure to use sanitizeResources: false for async HTTPS tests. ✏️ 🟢
/tmp/workspace/tests/node_compat/config.toml Enabled two new Node.js compatibility tests for HTTP client abort scenarios with keepAlive. ✏️ 🟢
Architecture Impact
  • New Patterns: Socket pool management and reuse pattern, Oneshot channel for resource hand-off between async tasks, State flag manipulation for object reuse, Keep-alive connection detection and specialized handling
  • Dependencies: Added: deno_core::futures::channel::oneshot import, Added: deno_net::io::TcpStreamResource import, Added: deno_net::ops_tls::TlsStreamResource import
  • Coupling: Increased coupling between HTTP ops and socket resource management; tighter integration between Node.js HTTP polyfill and network layer for socket state management and reuse.

Risk Areas: Socket state mutation: Complex manipulation of socket._readableState and _writableState flags could cause unexpected behavior if state flags are misaligned, Resource lifecycle: Socket reuse requires careful handling of resource cleanup and attachment to prevent premature destruction or memory leaks, Upgrade request detection: WebSocket upgrade detection via header matching must correctly distinguish upgrade requests from normal keepAlive requests, Error handling: Stale connection errors are remapped to ECONNRESET - incorrect remapping could break client retry logic, TLS socket reuse: Complex interaction between TLS and TCP socket reuse with encrypted flag tracking, Resource leaks: If socket reclamation fails (e.g., receiver dropped), socket resources might leak or become orphaned, Platform-specific code paths: Different handling for Unix sockets, Vsock, and Tunnel types - untested code paths could fail silently

Suggestions
  • Add comprehensive comments explaining the socket state reset logic, particularly the bitwise flag operations
  • Consider adding logging/debugging utilities for socket reuse lifecycle to help diagnose future issues
  • Add bounds checking or validation for socket state flags to catch misalignment early
  • Document the assumptions about kStreamBaseField and how it interacts with socket reuse
  • Consider adding metrics/counters to track socket reuse success/failure rates
  • Add tests for error scenarios: connection drops during reuse, partial body reads, aborted requests
  • Verify platform-specific code paths (Unix, Vsock, Tunnel) have proper test coverage or clear documentation of limitations
  • Document the timing requirements for socket reclamation (must happen before stream closes)

Full review in progress... | Powered by diffray


#[op2(async)]
#[serde]
pub async fn op_node_http_fetch_response_upgrade(

This comment was marked as spam.


// It's possible that the socket will be destroyed, and removed from
// any messages, before ever calling this. In that case, just skip
// it, since something else is destroying this connection anyway.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Logic error: always-true condition in _destroy
Agent: bugs

Category: bug

Description:
Line 1263 sets this.complete = true, then line 1264 checks !this.readableEnded || !this.complete. Since complete is always true at this point, !this.complete is always false. The condition reduces to just !this.readableEnded.

Suggestion:
Review the logic - if the intent is to check if response was prematurely terminated, the check should probably occur before setting complete=true, or the condition should use &&.

Confidence: 75%
Rule: qual_inverted_logic_js
Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +1254 to +1256
} catch (_e) {
// Socket reuse is best-effort.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Error suppression without logging in socket reuse
Agent: react

Category: quality

Description:
The catch block catches all errors with comment 'Socket reuse is best-effort' but provides no logging. While intentional, errors here could indicate socket pooling issues.

Suggestion:
Add debug logging: catch (_e) { debug('socket_reuse_failed', _e); /* best-effort */ }

Confidence: 65%
Rule: ts_log_errors_instead_of_failing_silently
Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +414 to +417
/// Returns the socket after the HTTP response body has been fully consumed.
/// This enables keepAlive connection reuse for the Node.js HTTP Agent.
/// Returns the new resource ID for the socket, or None if the connection
/// cannot be reused (e.g., connection error or already retrieved).

This comment was marked as spam.

Comment on lines +1169 to 1180
core.read(this._bodyRid, buf).then(async (bytesRead) => {
if (bytesRead === 0) {
// Return the socket to the agent pool BEFORE pushing null.
// This must happen before the stream ends, otherwise the socket
// may already be marked as destroyed.
await this.#tryReturnSocket();
this.push(null);
} else {
this.push(Buffer.from(buf.subarray(0, bytesRead)));
}
});
}

This comment was marked as spam.

Comment on lines 535 to +559
@@ -543,8 +555,13 @@ class ClientRequest extends OutgoingMessage {
// Simulates "secure" event on TLSSocket
// This makes yarn v1's https client working
this.socket.authorized = true;
// Mark the socket as encrypted for keepAlive reuse detection
this.socket.encrypted = true;

This comment was marked as spam.

Comment on lines +1183 to +1257
async #tryReturnSocket() {
const socket = this.socket;
if (!socket) {
return;
}

const req = this.req;
// Only pool the socket if keepAlive is enabled.
if (!req?.shouldKeepAlive) {
return;
}

const handle = req?._socketHandle || socket._handle;
if (!handle) {
return;
}

try {
const newRid = await op_node_http_response_reclaim_conn(this._bodyRid);
if (newRid == null) {
return;
}

const remoteAddr = {
hostname: socket.remoteAddress || "",
port: socket.remotePort || 0,
};
const localAddr = {
hostname: socket.localAddress || "",
port: socket.localPort || 0,
};

const isTls = socket.encrypted === true;
const conn = isTls
? new TlsConn(newRid, remoteAddr, localAddr)
: new TcpConn(newRid, remoteAddr, localAddr);

handle[kStreamBaseField] = conn;
if (!socket._handle) {
socket._handle = handle;
}

// Reset socket state for reuse.
if (socket._readableState?.[kState] !== undefined) {
socket._readableState[kState] &= ~(kDestroyed | kErrored);
}
if (socket._writableState?.[kState] !== undefined) {
socket._writableState[kState] &=
~(kEnding | kEnded | kDestroyed | kErrored);
socket._writableState.writable = true;
}
handle.destroyed = false;

// Agent's 'free' handler checks socket._httpMessage.shouldKeepAlive.
if (req && !socket._httpMessage) {
socket._httpMessage = req;
}

// Remove error listener to prevent accumulation on reused sockets.
if (req?._socketErrorListener) {
socket.removeListener("error", req._socketErrorListener);
req._socketErrorListener = null;
}

socket.emit("free");

// Clear references so old request/response don't destroy the pooled socket.
if (req) {
req.socket = null;
}
this.socket = null;
} catch (_e) {
// Socket reuse is best-effort.
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Private method #tryReturnSocket lacks unit test coverage
Agent: testing

Category: quality

Description:
New private async method '#tryReturnSocket()' handles complex socket state reset including stream state bit manipulation and handle restoration. The code is complex (lines 1226-1244) and would benefit from focused unit tests.

Suggestion:
Add unit tests verifying socket state flags properly reset and error handling when reclaim fails

Confidence: 72%
Rule: test_new_parameter_coverage
Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray


// Stop reading and save handle for keepAlive restoration.
this.socket?._handle?.readStop();
this._socketHandle = this.socket?._handle;

This comment was marked as spam.

},
});

Deno.test("[node/https] request directly with key and cert as agent", async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Double type assertion 'as never as Type'
Agent: typescript

Category: bug

Description:
Using 'as never as net.AddressInfo' is a double assertion that completely bypasses TypeScript's type safety. This pattern is a code smell even if it works.

Suggestion:
Remove 'as never' and use proper type handling: (server.address() as net.AddressInfo | null)?.port with null check

Confidence: 80%
Rule: ts_type_assertion_abuse
Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +1 to +88
// Test HTTP Agent keepAlive connection reuse
import http from "node:http";
import { Agent } from "node:http";
import { strictEqual } from "node:assert";

const agent = new Agent({
keepAlive: true,
maxSockets: 1,
maxFreeSockets: 1,
});

const server = http.createServer((_req, res) => {
res.end("ok");
});

function makeRequest(path: string): Promise<{ reusedSocket: boolean }> {
return new Promise((resolve, reject) => {
const req = http.get(
{
host: "localhost",
port: (server.address() as { port: number }).port,
agent,
path,
},
(res) => {
const reusedSocket = req.reusedSocket;
res.on("data", () => {});
res.on("end", () => {
resolve({ reusedSocket });
});
},
);
req.on("error", reject);
});
}

function waitForFreeSockets(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 10));
}

server.listen(0, async () => {
const name = `localhost:${(server.address() as { port: number }).port}:`;

// First request - should create new socket
const first = await makeRequest("/first");
strictEqual(
first.reusedSocket,
false,
"First request should not reuse socket",
);
console.log("First request: reusedSocket =", first.reusedSocket);

// Wait for socket to be returned to pool
await waitForFreeSockets();
strictEqual(
agent.freeSockets[name]?.length,
1,
"Socket should be in freeSockets pool",
);
console.log("Socket in freeSockets pool:", agent.freeSockets[name]?.length);

// Second request - should reuse socket
const second = await makeRequest("/second");
strictEqual(second.reusedSocket, true, "Second request should reuse socket");
console.log("Second request: reusedSocket =", second.reusedSocket);

// Wait for socket to be returned to pool
await waitForFreeSockets();
strictEqual(
agent.freeSockets[name]?.length,
1,
"Socket should be back in freeSockets pool",
);
console.log(
"Socket back in freeSockets pool:",
agent.freeSockets[name]?.length,
);

// Third request - should still reuse socket
const third = await makeRequest("/third");
strictEqual(third.reusedSocket, true, "Third request should reuse socket");
console.log("Third request: reusedSocket =", third.reusedSocket);

console.log("All keepAlive tests passed!");

server.close();
agent.destroy();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 LOW - HTTP keepAlive spec test lacks error scenario coverage
Agent: testing

Category: quality

Description:
The keepAlive spec test only tests happy path socket reuse with three sequential GET requests. No error handling scenarios (connection errors, timeouts) are covered.

Suggestion:
Extend test coverage to include error scenarios and different HTTP methods (POST, PUT)

Confidence: 60%
Rule: test_new_parameter_coverage
Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@diffray-bot
Copy link
Copy Markdown

Review Summary

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Validated 78 issues: 32 kept, 46 filtered

Issues Found: 32

💬 See 14 individual line comment(s) for details.

📊 18 unique issue type(s) across 32 location(s)

📋 Full issue list (click to expand)

🟠 HIGH - Floating promise in _writeHeader IIFE (4 occurrences)

Agent: nodejs

Category: bug

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:504-738 The async IIFE that handles HTTP request sending is fire-and-forget. Errors inside are caught and em... Consider storing the promise or adding more explicit lifecycle tracking for testability and debuggin... 75%
ext/node/polyfills/http.ts:863-867 The async finish() function is called without await at lines 863 and 867. The promise is not tracked... Either await finish() or attach .catch() error handling. Consider using Promise.resolve(finish()).ca... 80%
ext/node/polyfills/http.ts:1169-1180 The promise from core.read().then() is not awaited or returned. If the then callback throws, it coul... Store the promise or add .catch() handler for explicit error handling and better observability. 70%
ext/node/polyfills/http.ts:567 The infoPromise.then() is called without error handling and the promise chain is not awaited. The ca... Add .catch() handler or properly manage the promise lifecycle for explicit error handling. 65%

Rule: node_floating_promise


🟠 HIGH - Incorrect finally handler invocation

Agent: nodejs

Category: bug

File: ext/node/polyfills/http.ts:2020-2023

Description: The code uses .finally(nextTick(onError, this, err, cb)) which calls nextTick() immediately, scheduling the callback right away rather than after the promise settles. nextTick() returns undefined, so .finally() receives undefined instead of a function.

Suggestion: Change to .finally(() => nextTick(onError, this, err, cb)) to ensure nextTick is called only after the promise settles.

Confidence: 95%

Rule: node_mixed_async_patterns


🟠 HIGH - unwrap() on downcast result can panic (3 occurrences)

Agent: rust

Category: bug

📍 View all locations
File Description Suggestion Confidence
ext/node/ops/http.rs:481 Using unwrap() on downcast result will panic if the upgraded stream is not the expected type TokioIo... Replace unwrap() with proper error handling using map_err() to convert the downcast error and propag... 85%
ext/node/ops/http.rs:481 Using expect() on Rc::try_unwrap() will panic if the resource still has other references. This is li... Replace expect() with proper error handling using map_err() and ? operator, similar to the pattern u... 90%
ext/node/ops/http.rs:675 Using unwrap() on stream.as_mut().next().await in library code. The comment explains the safety reas... Consider using expect() with a clearer error message or debug_assert! to validate the assumption. 60%

Rule: rust_unwrap_panic


🟠 HIGH - Empty catch blocks suppressing errors silently (3 occurrences)

Agent: react

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:849-859 Multiple nested catch blocks (lines 849, 857) are empty or contain only comments, completely suppres... Add at minimum debug logging even for intentionally suppressed errors to aid debugging: catch (err) ... 70%
ext/node/polyfills/http.ts:1254-1256 The catch block catches all errors with comment 'Socket reuse is best-effort' but provides no loggin... Add debug logging: catch (_e) { debug('socket_reuse_failed', _e); /* best-effort */ } 65%
ext/node/polyfills/http.ts:1671-1673 The catch block explicitly ignores errors without any logging. The comment explains it's for stream ... Add conditional debug logging for troubleshooting: catch (_) { /* stream closed - expected */ } 60%

Rule: ts_log_errors_instead_of_failing_silently


🟠 HIGH - Inconsistent optional chaining with direct property access (2 occurrences)

Agent: typescript

Category: bug

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:2197 Line 2197: request.url?.slice(request.url.indexOf(...)) uses optional chaining on slice but calls ... Use consistent optional chaining: req.url = request.url?.slice(request.url?.indexOf("/", 8) ?? 0); 85%
ext/node/polyfills/http.ts:2014 Line 2014 uses reader!.read() with non-null assertion. However, there's a guard at line 2009 `if (... The code is actually safe due to early return at line 2009. Remove the redundant ! assertion for cle... 60%

Rule: ts_non_null_assertion


🟠 HIGH - New property _socketErrorListener lacks test coverage (5 occurrences)

Agent: testing

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:807-809 A new property '_socketErrorListener' stores error listener references for cleanup on socket reuse. ... Consider adding tests verifying error listeners don't accumulate on reused sockets, though the imple... 70%
ext/node/polyfills/http.ts:535-559 New variables 'needsTlsUpgrade' (line 537) and socket.encrypted (line 559) support HTTPS keepAlive, ... Create tests/specs/node/https_agent_keepalive/ with HTTPS keepAlive tests to verify TLS socket reuse... 85%
ext/node/polyfills/http.ts:1183-1257 New private async method '#tryReturnSocket()' handles complex socket state reset including stream st... Add unit tests verifying socket state flags properly reset and error handling when reclaim fails 72%
ext/node/polyfills/http.ts:564 New private property '_socketHandle' stores the native socket handle for keepAlive restoration. Whil... Consider adding tests that verify _socketHandle is correctly stored and enables socket pool reclamat... 68%
tests/specs/node/http_agent_keepalive/main.ts:1-88 The keepAlive spec test only tests happy path socket reuse with three sequential GET requests. No er... Extend test coverage to include error scenarios and different HTTP methods (POST, PUT) 60%

Rule: test_new_parameter_coverage


🟠 HIGH - Double type assertion 'as never as Type'

Agent: typescript

Category: bug

File: tests/unit_node/http_no_cert_flag_test.ts:57

Description: Using 'as never as net.AddressInfo' is a double assertion that completely bypasses TypeScript's type safety. This pattern is a code smell even if it works.

Suggestion: Remove 'as never' and use proper type handling: (server.address() as net.AddressInfo | null)?.port with null check

Confidence: 80%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - Logic error: always-true condition in _destroy

Agent: bugs

Category: bug

File: ext/node/polyfills/http.ts:1261

Description: Line 1263 sets this.complete = true, then line 1264 checks !this.readableEnded || !this.complete. Since complete is always true at this point, !this.complete is always false. The condition reduces to just !this.readableEnded.

Suggestion: Review the logic - if the intent is to check if response was prematurely terminated, the check should probably occur before setting complete=true, or the condition should use &&.

Confidence: 75%

Rule: qual_inverted_logic_js


🟡 MEDIUM - JSDoc description slightly misleading

Agent: documentation

Category: docs

File: ext/node/ops/http.rs:414-417

Description: Documentation at line 414 says 'Returns the socket' but function actually returns Option. Line 416-417 clarify it returns the resource ID, but the first line is misleading.

Suggestion: Update first line to: 'Returns the resource ID for the socket after the HTTP response body has been fully consumed.'

Confidence: 75%

Rule: ts_jsdoc_description_mismatch


🟡 MEDIUM - Generic error message without context (2 occurrences)

Agent: quality

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:668 The error 'not implemented CONNECT' is thrown as a plain Error without using the project's custom er... Use a custom error class from ext:deno_node/internal/errors.ts, such as notImplemented() from _utils... 70%
tests/unit_node/http_test.ts:1954-1960 Test validates specific error message string 'getaddrinfo ENOTFOUND invalid-hostname.test' instead o... Check the error.code property (like 'ENOTFOUND') instead of relying on error message strings for mor... 60%

Rule: quality_missing_project_pattern


🟡 MEDIUM - Using 'any' type for socket properties

Agent: typescript

Category: quality

File: ext/node/polyfills/http.ts:1525-1528

Description: Lines 1526-1528 define _socketOverride and socket as 'any | null' with deno-lint-ignore comments. Using 'any' reduces type safety for socket operations.

Suggestion: Define proper Socket type or create a discriminated union type for multiple socket types to maintain type safety.

Confidence: 65%

Rule: ts_any_type_usage


🟡 MEDIUM - Error listener accumulation in IncomingMessageForServer

Agent: performance

Category: performance

File: ext/node/polyfills/http.ts:2031-2035

Description: An error listener is registered on socket via socket?.on('error', ...) in the constructor. For servers handling many requests, if sockets are reused without cleanup, listeners could accumulate.

Suggestion: Consider using socket?.once() instead of socket?.on(), or ensure explicit cleanup when the incoming message is destroyed.

Confidence: 60%

Rule: perf_event_listener_leak


🟡 MEDIUM - Unsafe Send/Sync implementations lack comprehensive safety documentation

Agent: rust

Category: quality

File: ext/node/ops/http.rs:711-714

Description: NodeHttpResourceToBodyAdapter has unsafe Send and Sync implementations with minimal safety documentation. The single-threaded assumption needs more explanation.

Suggestion: Add detailed safety documentation explaining the single-threaded executor context and consequences of misuse.

Confidence: 72%

Rule: rs_document_safety_panics_and_errors


🟡 MEDIUM - Missing documentation for public async operation

Agent: rust

Category: docs

File: ext/node/ops/http.rs:170-183

Description: Public async function op_node_http_request_with_conn lacks documentation describing its purpose, parameters, and error conditions.

Suggestion: Add doc comments explaining the function's purpose, parameters, and what FetchReturn contains. Include information about how keepAlive socket reuse works.

Confidence: 75%

Rule: rs_use_item_docs_and_module_crate_docs_with


🟡 MEDIUM - HTTP status codes as magic numbers without named constants (2 occurrences)

Agent: quality

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:1682 Multiple hardcoded HTTP status codes (101, 204, 205, 304) used for null-body detection. These are we... Consider using constants like HTTP_STATUS_SWITCHING_PROTOCOLS = 101, HTTP_STATUS_NO_CONTENT = 204, e... 60%
ext/node/polyfills/http.ts:394 The hardcoded magic number 91 (ASCII code for '[') is used for IPv6 address detection. While there's... Consider using String.charCodeAt('[') or a named constant for clarity. 62%

Rule: qual_magic_numbers_js


🟡 MEDIUM - Repeated array flattening in _addHeaderLines

Agent: react

Category: performance

File: ext/node/polyfills/http.ts:1294-1298

Description: Lines 1294 and 1298 both call .flat() on the headers array, creating two separate flattened arrays when one would suffice.

Suggestion: Flatten once at the start: const flatHeaders = headers.flat() and reuse for both rawHeaders and rawTrailers assignments.

Confidence: 75%

Rule: ts_optimize_chained_array_operations


🟡 MEDIUM - Unsafe array access without length check

Agent: typescript

Category: bug

File: ext/node/polyfills/http.ts:1828-1829

Description: Code checks ArrayIsArray(headers[0]) without first verifying headers has elements. If headers is empty, headers[0] returns undefined which is falsy, making the check fail harmlessly.

Suggestion: While not a crash risk, explicit length check improves readability: if (headers.length > 0 && ArrayIsArray(headers[0]))

Confidence: 62%

Rule: ts_empty_collection_not_handled


🔵 LOW - console.error for unimplemented feature

Agent: quality

Category: style

File: ext/node/polyfills/http.ts:2267-2270

Description: Server.setTimeout() uses console.error for a not-implemented feature. This is inconsistent with how other unimplemented features might be handled.

Suggestion: Consider throwing an error or using a consistent pattern for not-implemented features across the codebase.

Confidence: 65%

Rule: quality_avoid_console_in_production


ℹ️ 18 issue(s) outside PR diff (click to expand)

These issues were found in lines not modified in this PR.

🟠 HIGH - Floating promise in _writeHeader IIFE (2 occurrences)

Agent: nodejs

Category: bug

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:504-738 The async IIFE that handles HTTP request sending is fire-and-forget. Errors inside are caught and em... Consider storing the promise or adding more explicit lifecycle tracking for testability and debuggin... 75%
ext/node/polyfills/http.ts:863-867 The async finish() function is called without await at lines 863 and 867. The promise is not tracked... Either await finish() or attach .catch() error handling. Consider using Promise.resolve(finish()).ca... 80%

Rule: node_floating_promise


🟠 HIGH - Incorrect finally handler invocation

Agent: nodejs

Category: bug

File: ext/node/polyfills/http.ts:2020-2023

Description: The code uses .finally(nextTick(onError, this, err, cb)) which calls nextTick() immediately, scheduling the callback right away rather than after the promise settles. nextTick() returns undefined, so .finally() receives undefined instead of a function.

Suggestion: Change to .finally(() => nextTick(onError, this, err, cb)) to ensure nextTick is called only after the promise settles.

Confidence: 95%

Rule: node_mixed_async_patterns


🟠 HIGH - Empty catch blocks suppressing errors silently (2 occurrences)

Agent: react

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:849-859 Multiple nested catch blocks (lines 849, 857) are empty or contain only comments, completely suppres... Add at minimum debug logging even for intentionally suppressed errors to aid debugging: catch (err) ... 70%
ext/node/polyfills/http.ts:1671-1673 The catch block explicitly ignores errors without any logging. The comment explains it's for stream ... Add conditional debug logging for troubleshooting: catch (_) { /* stream closed - expected */ } 60%

Rule: ts_log_errors_instead_of_failing_silently


🟠 HIGH - Inconsistent optional chaining with direct property access (2 occurrences)

Agent: typescript

Category: bug

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:2197 Line 2197: request.url?.slice(request.url.indexOf(...)) uses optional chaining on slice but calls ... Use consistent optional chaining: req.url = request.url?.slice(request.url?.indexOf("/", 8) ?? 0); 85%
ext/node/polyfills/http.ts:2014 Line 2014 uses reader!.read() with non-null assertion. However, there's a guard at line 2009 `if (... The code is actually safe due to early return at line 2009. Remove the redundant ! assertion for cle... 60%

Rule: ts_non_null_assertion


🟡 MEDIUM - Generic error message without context (2 occurrences)

Agent: quality

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:668 The error 'not implemented CONNECT' is thrown as a plain Error without using the project's custom er... Use a custom error class from ext:deno_node/internal/errors.ts, such as notImplemented() from _utils... 70%
tests/unit_node/http_test.ts:1954-1960 Test validates specific error message string 'getaddrinfo ENOTFOUND invalid-hostname.test' instead o... Check the error.code property (like 'ENOTFOUND') instead of relying on error message strings for mor... 60%

Rule: quality_missing_project_pattern


🟡 MEDIUM - Using 'any' type for socket properties

Agent: typescript

Category: quality

File: ext/node/polyfills/http.ts:1525-1528

Description: Lines 1526-1528 define _socketOverride and socket as 'any | null' with deno-lint-ignore comments. Using 'any' reduces type safety for socket operations.

Suggestion: Define proper Socket type or create a discriminated union type for multiple socket types to maintain type safety.

Confidence: 65%

Rule: ts_any_type_usage


🟡 MEDIUM - Error listener accumulation in IncomingMessageForServer

Agent: performance

Category: performance

File: ext/node/polyfills/http.ts:2031-2035

Description: An error listener is registered on socket via socket?.on('error', ...) in the constructor. For servers handling many requests, if sockets are reused without cleanup, listeners could accumulate.

Suggestion: Consider using socket?.once() instead of socket?.on(), or ensure explicit cleanup when the incoming message is destroyed.

Confidence: 60%

Rule: perf_event_listener_leak


🟡 MEDIUM - Unsafe Send/Sync implementations lack comprehensive safety documentation

Agent: rust

Category: quality

File: ext/node/ops/http.rs:711-714

Description: NodeHttpResourceToBodyAdapter has unsafe Send and Sync implementations with minimal safety documentation. The single-threaded assumption needs more explanation.

Suggestion: Add detailed safety documentation explaining the single-threaded executor context and consequences of misuse.

Confidence: 72%

Rule: rs_document_safety_panics_and_errors


🟡 MEDIUM - HTTP status codes as magic numbers without named constants (2 occurrences)

Agent: quality

Category: quality

📍 View all locations
File Description Suggestion Confidence
ext/node/polyfills/http.ts:1682 Multiple hardcoded HTTP status codes (101, 204, 205, 304) used for null-body detection. These are we... Consider using constants like HTTP_STATUS_SWITCHING_PROTOCOLS = 101, HTTP_STATUS_NO_CONTENT = 204, e... 60%
ext/node/polyfills/http.ts:394 The hardcoded magic number 91 (ASCII code for '[') is used for IPv6 address detection. While there's... Consider using String.charCodeAt('[') or a named constant for clarity. 62%

Rule: qual_magic_numbers_js


🟡 MEDIUM - Repeated array flattening in _addHeaderLines

Agent: react

Category: performance

File: ext/node/polyfills/http.ts:1294-1298

Description: Lines 1294 and 1298 both call .flat() on the headers array, creating two separate flattened arrays when one would suffice.

Suggestion: Flatten once at the start: const flatHeaders = headers.flat() and reuse for both rawHeaders and rawTrailers assignments.

Confidence: 75%

Rule: ts_optimize_chained_array_operations


🟡 MEDIUM - Unsafe array access without length check

Agent: typescript

Category: bug

File: ext/node/polyfills/http.ts:1828-1829

Description: Code checks ArrayIsArray(headers[0]) without first verifying headers has elements. If headers is empty, headers[0] returns undefined which is falsy, making the check fail harmlessly.

Suggestion: While not a crash risk, explicit length check improves readability: if (headers.length > 0 && ArrayIsArray(headers[0]))

Confidence: 62%

Rule: ts_empty_collection_not_handled


🔵 LOW - console.error for unimplemented feature

Agent: quality

Category: style

File: ext/node/polyfills/http.ts:2267-2270

Description: Server.setTimeout() uses console.error for a not-implemented feature. This is inconsistent with how other unimplemented features might be handled.

Suggestion: Consider throwing an error or using a consistent pattern for not-implemented features across the codebase.

Confidence: 65%

Rule: quality_avoid_console_in_production


🔵 LOW - unwrap() on async stream next() with safety comment

Agent: rust

Category: bug

File: ext/node/ops/http.rs:675

Description: Using unwrap() on stream.as_mut().next().await in library code. The comment explains the safety reasoning but unwrap() in production code can be risky.

Suggestion: Consider using expect() with a clearer error message or debug_assert! to validate the assumption.

Confidence: 60%

Rule: rust_unwrap_panic



Review ID: 8e3324f7-5f2b-4f78-b80d-469377cc6e2b
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Copy link
Copy Markdown
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, nice work!

@fraidev fraidev merged commit 0905fd9 into denoland:main Dec 30, 2025
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slow AWS SDK requests in Deno compared to Node.js/Bun

4 participants