From 36886de1a3e4566ba35bfb26e4c81e5c252eac67 Mon Sep 17 00:00:00 2001 From: killagu Date: Thu, 22 Jan 2026 22:01:46 +0800 Subject: [PATCH 1/2] fix(fetch): prevent infinite retry loop on 401 responses `isTraversableNavigable()` was returning `true` unconditionally, which caused an infinite retry loop when the server responded with 401. In Node.js environment, there is no traversable navigable that can prompt the user for credentials (unlike browsers). The 401 retry logic at step 14 of HTTP-network-or-cache fetch requires user interaction to provide credentials, which is not possible in Node.js. Returning `false` disables the incomplete 401 retry logic that was causing the infinite loop, while preserving the URL credentials functionality added in #4747. Co-Authored-By: Claude Opus 4.5 --- lib/web/fetch/util.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index 5e51bdd35aa..c8eef60b8aa 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -1447,8 +1447,10 @@ function includesCredentials (url) { * @param {object|string} navigable */ function isTraversableNavigable (navigable) { - // TODO - return true + // Node.js is not a browser environment, there is no traversable navigable + // that can prompt the user for credentials. Returning true would cause + // infinite retry loops on 401 responses. + return false } class EnvironmentSettingsObjectBase { From c33ff335711753cc80da8a3953ba704ba3946e9e Mon Sep 17 00:00:00 2001 From: killagu Date: Thu, 22 Jan 2026 22:53:39 +0800 Subject: [PATCH 2/2] test(fetch): add test for 401 response not causing infinite loop Ensures that when a server responds with 401 Unauthorized, fetch does not enter an infinite retry loop. This test verifies the fix for isTraversableNavigable() returning false in Node.js environment. Co-Authored-By: Claude Opus 4.5 --- test/fetch/401-statuscode-no-infinite-loop.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/fetch/401-statuscode-no-infinite-loop.js diff --git a/test/fetch/401-statuscode-no-infinite-loop.js b/test/fetch/401-statuscode-no-infinite-loop.js new file mode 100644 index 00000000000..2b986f56d13 --- /dev/null +++ b/test/fetch/401-statuscode-no-infinite-loop.js @@ -0,0 +1,48 @@ +'use strict' + +const { fetch } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { test } = require('node:test') + +const { closeServerAsPromise } = require('../utils/node-http') + +test('Receiving a 401 status code should not cause infinite retry loop', async (t) => { + let requestCount = 0 + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + requestCount++ + res.statusCode = 401 + res.setHeader('WWW-Authenticate', 'Basic realm="test"') + res.end('Unauthorized') + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`) + + t.assert.strictEqual(response.status, 401) + t.assert.strictEqual(requestCount, 1, 'should only make one request, not retry infinitely') +}) + +test('Receiving a 401 status code with credentials include should not cause infinite retry loop', async (t) => { + let requestCount = 0 + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + requestCount++ + res.statusCode = 401 + res.setHeader('WWW-Authenticate', 'Basic realm="test"') + res.end('Unauthorized') + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`, { + credentials: 'include' + }) + + t.assert.strictEqual(response.status, 401) + t.assert.strictEqual(requestCount, 1, 'should only make one request, not retry infinitely') +})