Skip to content

fix: XHR adapter/avoid main-thread jank#10831

Open
LuckyPigDev wants to merge 2 commits into
axios:v1.xfrom
LuckyPigDev:fix/avoid-main-thread-jank
Open

fix: XHR adapter/avoid main-thread jank#10831
LuckyPigDev wants to merge 2 commits into
axios:v1.xfrom
LuckyPigDev:fix/avoid-main-thread-jank

Conversation

@LuckyPigDev
Copy link
Copy Markdown

Summary

Implemented asynchronous XSRF cookie reading using the Cookie Store API when available to avoid main-thread jank from synchronous document.cookie access in axios HTTP client.

Related issue

fixes: #10826

Problem

  • Axios reads XSRF cookies via document.cookie synchronously, which blocks the main thread
  • Under contention, this causes jank (performance issues) in browser performance traces
  • The synchronous nature of document.cookie API is a known performance bottleneck

Solution

Implemented a jank-avoiding read path for XSRF cookies that:

  1. Is XHR-only: Changes confined to XHR adapter where it actually helps (fetch adapter already async)
  2. Is automatic, not opt-in: Detects window.cookieStore at runtime, no new config options
  3. Round-trips cookie values correctly: Uses raw values from cookieStore.get() without decodeURIComponent
  4. Preserves existing security hardening: Maintains prototype-pollution guards and strict boolean checks
  5. Preserves non-standard-env no-op: Keeps fallback for Web Workers/React Native environments

Benefits

  • Reduced Jank: Avoids main-thread blocking in browsers with Cookie Store API support
  • Progressive Enhancement: Automatically uses faster API when available
  • No Breaking Changes: Fully backward compatible with existing code
  • Security Maintained: All CVE fixes and security hardening preserved

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

2 issues found across 4 files

Confidence score: 3/5

  • There is a concrete regression risk in lib/adapters/xhr.js: using Promise.resolve(config) can treat a config object with its own callable then as a thenable, which may hijack or block request dispatch.
  • The lib/helpers/cookies.js issue is low severity, but duplicated parsing/decoding in readAsync vs read could drift over time and cause inconsistent sync/async XSRF cookie behavior.
  • Given one medium-severity, high-confidence behavior risk (6/10, confidence 9/10), this looks like some merge risk rather than a merge-blocker.
  • Pay close attention to lib/adapters/xhr.js and lib/helpers/cookies.js - thenable coercion in config resolution and sync/async cookie parsing consistency.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/helpers/cookies.js">

<violation number="1" location="lib/helpers/cookies.js:37">
P3: Duplicate cookie parsing/decoding logic in `readAsync` can drift from `read`, creating inconsistent sync/async XSRF cookie handling.</violation>
</file>

<file name="lib/adapters/xhr.js">

<violation number="1" location="lib/adapters/xhr.js:18">
P2: Wrapping the resolved config in `Promise.resolve()` can treat a config object with an own callable `then` as a thenable and hijack or block request dispatch.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/adapters/xhr.js Outdated
Comment thread lib/helpers/cookies.js
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/adapters/xhr.js">

<violation number="1" location="lib/adapters/xhr.js:18">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

Behavior change in the XHR adapter is shipped without visible regression-test coverage.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/adapters/xhr.js
Copy link
Copy Markdown
Member

@jasonsaayman jasonsaayman left a comment

Choose a reason for hiding this comment

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

Thanks for the patch @John-pillo1124.

This is the right idea, but it doesn't match the constraints from #10826, and there are a few things to address before this can go in.

The fetch adapter changes need to come out:

lib/adapters/fetch.js is explicitly out of scope in the issue. Fetch is already async, so awaiting resolveConfig saves no jank; it just adds a microtask hop on every fetch request for no benefit. Revert this file entirely.

The XHR adapter is a near-total re-indent at +197/-179:

The issue is specific: do this without re-indenting the body. Wrapping everything in a handleConfig callback throws away git blame on the whole file. The shape we want is roughly:

async function (config) {
  const _config = await resolveConfig(config);
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // existing body unchanged, _config in closure
  });
}

No tests:

The issue calls this out: we need to mock window.cookieStore and verify the async branch actually runs, since asserting via document.cookie co-stores with cookieStore and doesn't differentiate. At minimum, I want to see:

  • cookieStore mock present, XHR request goes out with the XSRF header populated from the raw cookieStore value (no URI decoding).
  • cookieStore undefined, the existing document.cookie path is unchanged.
  • non-standard-env shim's new readAsync() returns Promise.resolve(null) (Web Worker / React Native fallback).
  • cookieStore.get rejection falls through to this.read(name) cleanly.

That fourth one matters because the .catch(() => this.read(name)) in readAsync swallows every error type silently. That fallback should be deliberate and covered.

resolveConfig is now polymorphic;

It returns the config synchronously on one branch and a Promise of the config on the other, depending on whether window.cookieStore is detected at runtime. That is a foot-gun for any future caller.

  • Keep resolveConfig synchronous and move the async cookieStore read into the XHR adapter directly, calling cookies.readAsync(xsrfCookieName) and setting the header there. This better matches the "XHR-only" framing in the issue.

A few non-blocking nits while you are in there:

  • lib/helpers/cookies.js:36 the typeof document === 'undefined' guard inside the standard-browser-env branch is dead code. The export is already gated on platform.hasStandardBrowserEnv, which means document exists. Drop it.
  • Trailing whitespace on several of the new lines (lib/helpers/cookies.js:36, lib/adapters/fetch.js:154, etc.) will likely fail lint once the full CI matrix runs.
  • The commit subject fix: XHR adapter/avoid main-thread jank uses / as a scope separator. Conventional commits want fix(xhr): avoid main-thread jank or similar. Squash the second fix-up commit into the first as well.
  • I would also like to see a microbenchmark or trace in the PR description showing the actual jank reduction. The original issue cites the WHATWG discussion as motivation, but doesn't quantify. 1000 sequential same-origin XHR requests with XSRF, before vs after a long task, would be enough to justify the change concretely.

@jasonsaayman jasonsaayman added priority::medium A medium priority commit::fix The PR is related to a bugfix status::changes-requested A reviewer requested changes to the PR labels May 1, 2026
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, When window.cookieStore exists, resolveConfig() returns a Promise and the XHR adapter defers request.open/send until it resolves, so axios() no longer synchronously dispatches the XHR. This breaks in-repo browser tests (and potentially user code) that assumes the request has been sent immediately after calling axios().

Severity: action required | Category: correctness

How to fix: Avoid async XHR dispatch

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

In cookieStore-capable browsers, the XHR adapter now defers request.open()/request.send() until resolveConfig()’s Promise resolves, changing adapter dispatch from synchronous to asynchronous.

Issue Context

Some in-repo browser tests and potential user code depend on the XHR being sent synchronously after invoking axios() (i.e., XHR side-effects occur in the same tick).

Fix Focus Areas

  • lib/helpers/resolveConfig.js[75-107]
  • lib/adapters/xhr.js[17-242]

Suggested direction

Keep resolveConfig() synchronous for XHR, or move async cookieStore reading into the XHR adapter in a way that does not delay dispatch (e.g., set XSRF header synchronously from document.cookie for XHR, or only use cookieStore in adapters that are already async, or make request dispatch async everywhere and update tests+documented behavior accordingly).

We noticed a couple of other issues in this PR as well - happy to share if helpful.


Qodo code review - free for open-source.

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 priority::medium A medium priority status::changes-requested A reviewer requested changes to the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XHR adapter: avoid main-thread jank from synchronous document.cookie reads (use cookieStore where available)

3 participants