Visitar URL original
fix(interceptors): tolerate nullish handlers in syncHandlerEntries by r0h1tb · Pull Request #11118 · axios/axios · GitHub
Skip to content

fix(interceptors): tolerate nullish handlers in syncHandlerEntries - #11118

Merged
jasonsaayman merged 3 commits into
axios:v1.xfrom
r0h1tb:fix/interceptor-nullish-handlers
Aug 13, 2026
Merged

jasonsaayman merged 3 commits into
axios:v1.xfrom
r0h1tb:fix/interceptor-nullish-handlers

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

🏄

Fixes #11114

Problem

syncHandlerEntries() reads handlers.length before checking that handlers exists, so assigning a falsy value to the public handlers array makes every subsequent request throw:

axios.interceptors.request.use(config => config);
axios.interceptors.request.handlers = null;
await axios.get('https://example.com');
// TypeError: Cannot read properties of null (reading 'length')

clear() has always guarded with if (this.handlers), and utils.forEach returns early on nullish input, so a falsy handlers array was a tolerated state on v1.19.0. This is a regression from #11087 (merged, unreleased).

Fix

Three reads were unguarded, not one:

  • syncHandlerEntries — the else if (handlers.length !== ...) branch and the trailing internals.handlersLength = handlers.length. Both now use a countHandlers() helper that treats nullish as an empty stack.
  • trimHandlers — returns early instead of dereferencing.
  • the forEach finally block — internals.handlersLength = this.handlers.length had the same problem and now goes through the same helper.

Fixing only syncHandlerEntries leaves the finally block throwing on the way out of the first forEach, so both paths are covered.

Tests

8 cases, parametrized over null and undefined:

  • forEach is a no-op rather than throwing
  • a repeated forEach stays safe — this takes the handlers === handlersRef branch, which is where the stack trace in the report actually lands, and it isn't reached by a single call
  • eject does not throw
  • the manager recovers once handlers is restored to an array

All 8 fail on v1.x with the reported TypeError and pass with this change.

Verification

npx vitest run --project unit: 1001 passed, 1 failed. The single failure is tests/unit/adapters/fetch.test.js > should not use inherited Symbol.iterator for request headers, which fails identically on a clean checkout — unrelated to this change. npx eslint lib/core/InterceptorManager.js is clean.

Not included

The report mentions two further items from the same change, deliberately left out to keep this focused on the crash — happy to follow up on either:

  • eject(String(id)) silently no-ops now that lookups go through a Map rather than an array index.
  • tests/unit/core/InterceptorManager.test.js:32 indexes handlers by id, which only holds while nothing below those slots is trimmed.

No PRE_RELEASE_CHANGELOG.md entry: per AGENTS.md that file tracks user-visible unreleased changes, and this crash never shipped — v1.19.0 behaves correctly. Happy to add one if you'd prefer it recorded.


Summary by cubic

Prevents a TypeError when interceptor handlers are set to null or undefined by treating them as empty and restoring them on use(). Previously syncHandlerEntries and the forEach cleanup read .length unguarded and could crash; now both paths are safe and behavior matches prior tolerant releases.

Details

  • Adds countHandlers() and uses it in syncHandlerEntries and the forEach finally block to guard nullish stacks.
  • Makes trimHandlers() return early for nullish handlers.
  • Updates use() to initialize a nullish handlers array to [] so adding a handler recovers the manager.
  • Changes are limited to lib/core/InterceptorManager.js.

Testing and Docs

  • Adds 8 unit tests covering null/undefined: forEach is a no-op, repeated forEach is safe, eject does not throw, and adding a handler after a nullish stack works.
  • Suggest updating /docs/ to clarify that while handlers is public, consumers should prefer eject()/clear(); if set to null/undefined, axios treats it as empty and use() restores it.
  • Semantic version impact: Patch.

Written for commit a56dd3c. Summary will update on new commits.

Review in cubic

syncHandlerEntries read handlers.length before checking that handlers
exists, so assigning a falsy value to the public handlers array made every
subsequent request throw:

    TypeError: Cannot read properties of null (reading 'length')
        at syncHandlerEntries (lib/core/InterceptorManager.js:31:39)
        at InterceptorManager.forEach (lib/core/InterceptorManager.js:135:5)

clear() has always guarded with `if (this.handlers)`, and utils.forEach
returns early on nullish input, so a falsy handlers array was a tolerated
state before axios#11087. Both branches of syncHandlerEntries and the trailing
handlersLength assignment now treat nullish as an empty stack, and
trimHandlers returns early rather than dereferencing.

The forEach finally block had the same unguarded read; it now goes through
the same helper.

Regression tests cover null and undefined across forEach, a repeated
forEach (which takes the handlers === handlersRef branch where the reported
stack trace landed), eject, and recovery once handlers is restored.
@r0h1tb
r0h1tb requested a review from jasonsaayman as a code owner August 1, 2026 10:12

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Greptile Summary

This change restores a nullish interceptor handler stack before registering a new interceptor. A focused runtime check covered both null and undefined replacement after prior registrations: the new handler received the next ID, was iterable and invocable, and remained intact when stale IDs were ejected. The same scenario throws before this change and succeeds with it; the focused InterceptorManager unit tests also pass.

Confidence Score: 5/5

The PR is safe to merge; no blocking failure remains.

The changed recovery path was exercised directly for both nullish handler-stack values and verified against the focused unit suite. The pre-change failure no longer occurs.

T-Rex T-Rex Logs

What T-Rex did

    • Ran a focused Node validation script against the immediate pre-change revision and observed a TypeError when it attempted to push onto a null handler collection.
    • Ran the same script on the PR head with prior registrations where handlers could be null or undefined; the validation showed the collection was restored, the next ID assigned, the replacement handler iterated and invoked, and the collection remained after stale-ID ejection.
    • Ran the focused InterceptorManager Vitest suite and observed 16 passing tests, confirming the nullish recovery behavior works without breaking the covered interceptor contract.
    • Documented the defect location at lib/core/InterceptorManager.js:80 in the pre-PR revision and the PR fix at lib/core/InterceptorManager.js:76-78, noting that validation artifacts were uploaded without modifying repository sources.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (3): Last reviewed commit: "fix(interceptors): restore nullish stack..." | Re-trigger Greptile

@jasonsaayman
jasonsaayman merged commit 2d2a21a into axios:v1.x Aug 13, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commit::fix The PR is related to a bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forEach() throws when interceptors.handlers is set to null

2 participants