From 5620df16961f5c9568cdd5d5f94009c37ca43ec5 Mon Sep 17 00:00:00 2001 From: Travis Bonnet Date: Thu, 19 Mar 2026 11:15:03 -0500 Subject: [PATCH] fix(mock): improve error message when intercepts are exhausted When all mock intercepts for a request have been consumed, the error message now includes how many interceptors remain and how many were originally defined. This helps users understand that their intercepts were used up rather than misconfigured. Example: "Mock dispatch not matched for path '/foo': subsequent request to origin https://api.example.com was not allowed (net.connect disabled), 0 interceptor(s) remaining out of 3 defined" Fixes #2219 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/mock/mock-symbols.js | 3 +- lib/mock/mock-utils.js | 12 ++++++-- test/mock-agent.js | 59 ++++++++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/lib/mock/mock-symbols.js b/lib/mock/mock-symbols.js index 940dbe6e3f8..9b23e8e3cf0 100644 --- a/lib/mock/mock-symbols.js +++ b/lib/mock/mock-symbols.js @@ -27,5 +27,6 @@ module.exports = { kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'), kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'), kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), - kMockCallHistoryAddLog: Symbol('mock call history add log') + kMockCallHistoryAddLog: Symbol('mock call history add log'), + kTotalDispatchCount: Symbol('total dispatch count') } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 291a85753be..a51816f4082 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -6,7 +6,8 @@ const { kMockAgent, kOriginalDispatch, kOrigin, - kGetNetConnect + kGetNetConnect, + kTotalDispatchCount } = require('./mock-symbols') const { serializePathWithQuery } = require('../core/util') const { STATUS_CODES } = require('node:http') @@ -206,6 +207,8 @@ function addMockDispatch (mockDispatches, key, data, opts) { const replyData = typeof data === 'function' ? { callback: data } : { ...data } const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } mockDispatches.push(newMockDispatch) + // Track total number of intercepts ever registered for better error messages + mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1 return newMockDispatch } @@ -401,13 +404,16 @@ function buildMockDispatch () { } catch (error) { if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') { const netConnect = agent[kGetNetConnect]() + const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length + const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length + const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined` if (netConnect === false) { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`) } if (checkNetConnect(netConnect, origin)) { originalDispatch.call(this, opts, handler) } else { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`) } } else { throw error diff --git a/test/mock-agent.js b/test/mock-agent.js index bb3d2afb9f7..dbc8ad1836d 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2464,7 +2464,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for path await t.assert.rejects(request(`${baseUrl}/wrong`, { method: 'GET' - }), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for path '/wrong': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for method and the origin was not allowed by net connect', async (t) => { @@ -2497,7 +2497,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for meth await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'WRONG' - }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for method 'WRONG' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for body and the origin was not allowed by net connect', async (t) => { @@ -2532,7 +2532,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for body await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET', body: 'wrong' - }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for body 'wrong' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - enableNetConnect should throw if dispatch not matched for headers and the origin was not allowed by net connect', async (t) => { @@ -2571,7 +2571,7 @@ test('MockAgent - enableNetConnect should throw if dispatch not matched for head headers: { 'User-Agent': 'wrong' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"User-Agent":"wrong"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect is not enabled for this origin), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - disableNetConnect should throw if dispatch not found by net connect', async (t) => { @@ -2606,12 +2606,10 @@ test('MockAgent - disableNetConnect should throw if dispatch not found by net co await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET' - }), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) }) test('MockAgent - headers function interceptor', async (t) => { - t.plan(8) - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { t.assert.fail('should not be called') res.end('should not be called') @@ -2647,12 +2645,12 @@ test('MockAgent - headers function interceptor', async (t) => { headers: { Authorization: 'Bearer foo' } - }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '{"Authorization":"Bearer foo"}' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) await t.assert.rejects(request(`${baseUrl}/foo`, { method: 'GET', headers: ['Authorization', 'Bearer foo'] - }), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled)`)) + }), new MockNotMatchedError(`Mock dispatch not matched for headers '["Authorization","Bearer foo"]' on path '/foo': subsequent request to origin ${baseUrl} was not allowed (net.connect disabled), 1 interceptor(s) remaining out of 1 defined`)) { const { statusCode } = await request(`${baseUrl}/foo`, { @@ -2672,6 +2670,49 @@ test('MockAgent - headers function interceptor', async (t) => { } }) +test('MockAgent - should include intercept count in error when intercepts are exhausted', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + t.assert.fail('should not be called') + res.end('should not be called') + }) + after(() => { + server.closeAllConnections?.() + server.close() + }) + + await once(server.listen(0), 'listening') + + const baseUrl = `http://localhost:${server.address().port}` + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + after(() => mockAgent.close()) + + const mockPool = mockAgent.get(baseUrl) + mockPool.intercept({ + path: '/foo', + method: 'POST' + }).reply(200, 'foo') + + mockAgent.disableNetConnect() + + // First request consumes the only intercept + const { statusCode } = await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.assert.strictEqual(statusCode, 200) + + // Second request should fail with a message indicating intercept counts + try { + await request(`${baseUrl}/foo`, { + method: 'POST' + }) + t.assert.fail('should have thrown') + } catch (err) { + t.assert.ok(err.message.includes('0 interceptor(s) remaining out of 1 defined'), `Error message should include interceptor counts, got: ${err.message}`) + } +}) + test('MockAgent - clients are not garbage collected', async (t) => { const samples = 250 t.plan(2)