Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
52 changes: 36 additions & 16 deletions lib/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const { InvalidArgumentError } = require('./core/errors')
const Agent = require('./dispatcher/agent')
const Dispatcher1Wrapper = require('./dispatcher/dispatcher1-wrapper')

// Fallback storage for when globalThis is not extensible (e.g. frozen)
let fallbackDispatcher

if (getGlobalDispatcher() === undefined) {
setGlobalDispatcher(new Agent())
}
Expand All @@ -17,25 +20,42 @@ function setGlobalDispatcher (agent) {
throw new InvalidArgumentError('Argument agent must implement Agent')
}

Object.defineProperty(globalThis, globalDispatcher, {
value: agent,
writable: true,
enumerable: false,
configurable: false
})

const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)

Object.defineProperty(globalThis, legacyGlobalDispatcher, {
value: legacyAgent,
writable: true,
enumerable: false,
configurable: false
})
try {
Object.defineProperty(globalThis, globalDispatcher, {
value: agent,
writable: true,
enumerable: false,
configurable: false
})
} catch (err) {
// globalThis is not extensible (e.g. Object.freeze(globalThis))
// Use fallback storage instead
if (err instanceof TypeError) {
fallbackDispatcher = agent
return
}
throw err
}

try {
const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent)

Object.defineProperty(globalThis, legacyGlobalDispatcher, {
value: legacyAgent,
writable: true,
enumerable: false,
configurable: false
})
} catch (err) {
// globalThis is not extensible; fallback storage is already set
if (!(err instanceof TypeError)) {
throw err
}
}
}

function getGlobalDispatcher () {
return globalThis[globalDispatcher]
return globalThis[globalDispatcher] ?? fallbackDispatcher
}

// These are the globals that can be installed by undici.install().
Expand Down
62 changes: 62 additions & 0 deletions test/global-frozen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'

const { tspl } = require('@matteo.collina/tspl')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please use t.plan()

const { test } = require('node:test')

test('frozen globalThis - setGlobalDispatcher succeeds', (t) => {
t = tspl(t, { plan: 2 })

// Freeze globalThis
Object.freeze(globalThis)

// Dynamically require inside test to get fresh module state
// in a frozen globalThis context
const { setGlobalDispatcher, getGlobalDispatcher } = require('../lib/global')
const Agent = require('../lib/dispatcher/agent')

// Create a new dispatcher and set it - should not throw
const newAgent = new Agent()
let setError = null
try {
setGlobalDispatcher(newAgent)
} catch (err) {
setError = err
}

t.ifError(setError, 'setGlobalDispatcher should not throw with frozen globalThis')

// Verify we can retrieve a dispatcher
const retrieved = getGlobalDispatcher()
t.ok(retrieved, 'getGlobalDispatcher should return a dispatcher')
})

test('frozen globalThis - graceful degradation', (t) => {
t = tspl(t, { plan: 1 })

// globalThis is already frozen from previous test
const { getGlobalDispatcher } = require('../lib/global')

// Should still be able to get a dispatcher without errors
const dispatcher = getGlobalDispatcher()
t.ok(dispatcher !== null && dispatcher !== undefined, 'getGlobalDispatcher should return a valid dispatcher even with frozen globalThis')
})

test('frozen globalThis - fallback dispatcher persists', (t) => {
t = tspl(t, { plan: 2 })

// globalThis is already frozen from previous tests
const { getGlobalDispatcher, setGlobalDispatcher } = require('../lib/global')
const Agent = require('../lib/dispatcher/agent')

// Get current dispatcher
const dispatcher1 = getGlobalDispatcher()
t.ok(dispatcher1, 'First call to getGlobalDispatcher returns dispatcher')

// Set a new one
const newAgent = new Agent()
setGlobalDispatcher(newAgent)

// Get again - should return the one we just set (from fallback)
const dispatcher2 = getGlobalDispatcher()
t.equal(dispatcher2, newAgent, 'getGlobalDispatcher returns dispatcher set in frozen globalThis')
})
Loading