Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,13 @@ async function httpNetworkOrCacheFetch (
if (request.body != null) {
// 1. If request’s body’s source is null, then return a network error.
if (request.body.source == null) {
return makeNetworkError('expected non-null body source')
// Note: In Node.js, this code path should not be reached because
// isTraversableNavigable() returns false for non-navigable contexts.
// However, we handle it gracefully by returning the response instead of
// a network error, as we won't actually retry the request.
// This aligns with the Fetch spec discussion in whatwg/fetch#1132,
// which allows implementations flexibility when credentials can't be obtained.
return response
}

// 2. Set request’s body to the body of the result of safely extracting
Expand Down
6 changes: 4 additions & 2 deletions lib/web/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1447,8 +1447,10 @@ function includesCredentials (url) {
* @param {object|string} navigable
*/
function isTraversableNavigable (navigable) {
// TODO
return true
// Returns true only if we have an actual traversable navigable object
// that can prompt the user for credentials. In Node.js, this will always
// be false since there's no Window object or navigable.
return navigable != null && navigable !== 'client' && navigable !== 'no-traversable'
}

class EnvironmentSettingsObjectBase {
Expand Down
44 changes: 44 additions & 0 deletions test/fetch/401-statuscode-no-infinite-loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,47 @@ test('Receiving a 401 status code should not cause infinite retry loop', async (
const response = await fetch(`http://localhost:${server.address().port}`)
assert.strictEqual(response.status, 401)
})

test('Receiving a 401 status code should not fail for stream-backed request bodies', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 401
res.end('Unauthorized')
}).listen(0)

t.after(closeServerAsPromise(server))
await once(server, 'listening')

const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'PUT',
duplex: 'half',
body: new ReadableStream({
start (controller) {
controller.enqueue(Buffer.from('hello world'))
controller.close()
}
})
})

assert.strictEqual(response.status, 401)
})

test('Receiving a 401 status code should work for POST with JSON body', async (t) => {
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.statusCode = 401
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ error: 'unauthorized' }))
}).listen(0)

t.after(closeServerAsPromise(server))
await once(server, 'listening')

const response = await fetch(`http://localhost:${server.address().port}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: 'test' })
})

assert.strictEqual(response.status, 401)
const body = await response.json()
assert.deepStrictEqual(body, { error: 'unauthorized' })
})
Loading