Skip to content

Conversation

@Flo0806
Copy link
Member

@Flo0806 Flo0806 commented Sep 26, 2025

🔗 Linked issue

Fixes #33274
resolves #33240
resolves #33369

When two useFetch calls generate the same key and are refreshed by reactive data changes,
one of them may throw:

Error: [GET] "/api/example?count=1": Request aborted as another request to the same endpoint was initiated.

📚 Description

This happens because both the key watcher and the params watcher can trigger execute() in the same flush cycle, leading to overlapping requests.

Changes

  • Add a keyChanging guard that is set during a key migration
  • Reset the guard with queuePostFlushCb after the current flush
  • Ensure migration of existing data happens before unregistering the old key

Why

  • Prevents duplicate execute() during key switches
  • Preserves the intended semantics:
    • Key watcher: flush: 'sync' → immediate and deterministic
    • Params watcher: default flush (pre) → batched, non-synchronous (covered by tests)

This resolves the regression reported in #33274 while keeping the new test
(should watch params deeply in a non synchronous way) passing.

@Flo0806 Flo0806 requested a review from danielroe as a code owner September 26, 2025 08:42
@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 26, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33325

nuxt

npm i https://pkg.pr.new/nuxt@33325

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33325

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33325

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33325

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33325

commit: aa36a74

@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

Walkthrough

The diff updates packages/nuxt/src/app/composables/asyncData.ts to import Vue's queuePostFlushCb, introduce a local keyChanging flag, and rework the key watcher to read existing data from the old key, initialise the new key with preserved or cached data, and perform the actual key switch after the current flush. Migration logic can trigger fetch for the new key depending on prior state, cancels/unregisters the old key safely, and resets the concurrency guard via queuePostFlushCb. The params watcher short-circuits while keyChanging is true. Scope disposal now unregisters both the key and params watchers. Two regression tests were added in test/nuxt/use-async-data.test.ts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the main fix by highlighting the prevention of duplicate execution on key changes within useAsyncData, directly reflecting the core change introduced in the code.
Linked Issues Check ✅ Passed The code adds a keyChanging guard, uses queuePostFlushCb to reset it, and enhances watcher and migration logic to avoid overlapping execute() calls and ensure correct reactive updates, thereby fulfilling the coding requirements of issues #33274, #33240, and #33369.
Out of Scope Changes Check ✅ Passed All modifications are focused on the useAsyncData composable and its related tests to address the linked bug reports, with no unrelated or out-of-scope code alterations observed.
Description Check ✅ Passed The description directly references the linked issues, explains the bug scenario and its cause, and outlines the specific changes made to resolve overlapping requests, demonstrating clear relevance to the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae6c3c1 and aa36a74.

📒 Files selected for processing (1)
  • test/nuxt/use-async-data.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • test/nuxt/use-async-data.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Write unit tests for core functionality using vitest

Files:

  • test/nuxt/use-async-data.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.
📚 Learning: 2025-09-10T14:42:56.647Z
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.

Applied to files:

  • test/nuxt/use-async-data.test.ts
🧬 Code graph analysis (1)
test/nuxt/use-async-data.test.ts (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
  • useAsyncData (184-402)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-benchmark
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: test-size
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: code
🔇 Additional comments (2)
test/nuxt/use-async-data.test.ts (2)

806-846: Excellent regression test for issue #33274.

This test precisely reproduces the bug scenario where an external watch defined before useAsyncData with a computed key caused duplicate executions. The assertions correctly verify:

  • Initial execution with empty string
  • Two subsequent key changes trigger exactly 2 additional calls (not 3, which would indicate the duplicate execution bug)
  • No "Request aborted" errors occur
  • Data values are correctly updated after each change

The test structure and assertions align perfectly with the PR objectives.


848-887: Well-structured complementary test case.

This test complements the previous test by additionally tracking an external watch spy to verify that:

  • The external watcher is invoked the expected number of times (2 changes)
  • Internal useAsyncData watchers execute correctly (3 times: initial + 2 changes)
  • No race conditions or overlapping requests occur (verified by error.value assertions)

The test confirms that the fix maintains correct behaviour for both internal and external watchers without interference.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)

340-348: Optional: mirror migrated value into payload for consistency

When seeding the new key from oldKey’s in-memory value (hadData), consider also setting nuxtApp.payload.data[newKey] to keep payload/devtools in sync until the next fetch.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47ea684 and 7442641.

📒 Files selected for processing (1)
  • packages/nuxt/src/app/composables/asyncData.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/composables/asyncData.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.
📚 Learning: 2025-09-10T14:42:56.647Z
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.

Applied to files:

  • packages/nuxt/src/app/composables/asyncData.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: test-fixtures (windows-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: test-size
  • GitHub Check: test-benchmark
  • GitHub Check: code
🔇 Additional comments (5)
packages/nuxt/src/app/composables/asyncData.ts (5)

1-1: Import of queuePostFlushCb is appropriate

Using queuePostFlushCb to reset guards post-flush is the right tool for this watcher timing issue.


196-196: Guard flag to prevent overlapping executes

Introducing keyChanging is a simple and effective concurrency guard during key migration.


327-366: Key migration ordering and post-flush guard look correct

  • Creating the new container before unregistering the old one avoids data loss.
  • Passing cachedData only when sourced from getCachedData is sound.
  • Post-flush reset via queuePostFlushCb prevents params watcher from double-firing in the same cycle.

Based on learnings


358-360: Confirm behaviour: executing on key migration when immediate is false

Triggering execute when hadData or wasRunning even if options.immediate is false may surprise consumers expecting no automatic fetches. Please confirm this matches desired semantics and existing tests cover it.


379-383: Proper cleanup of both watchers

Unsubscribing both key and params watchers on scope dispose is correct and prevents leaks.

Comment on lines +379 to 375
onScopeDispose(() => {
unsubKeyWatcher()
unsubWatcher()
unsubParamsWatcher()
unregister(key.value)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: params watcher calls a stale container after a key change

The params watcher invokes asyncData._execute, which is bound to the initial container. After a key change, it should target the current container or it may no longer refresh correctly.

Apply this diff:

-      ? watch(options.watch, () => {
-          if (keyChanging) { return } // avoid double execute while the key switch is being processed
-          asyncData._execute({ cause: 'watch', dedupe: options.dedupe })
-        })
+      ? watch(options.watch, () => {
+          if (keyChanging) { return } // avoid double execute while the key switch is being processed
+          nuxtApp._asyncData[key.value]!._execute({ cause: 'watch', dedupe: options.dedupe })
+        })
🤖 Prompt for AI Agents
In packages/nuxt/src/app/composables/asyncData.ts around lines 371-375, the
watcher callback currently calls the originally bound asyncData._execute which
sticks to the initial container; change the callback to resolve the
current/active asyncData container at invocation time and call that container's
_execute with the same args (i.e., avoid invoking a pre-bound method tied to the
old container). Implement this by looking up the live container on each watch
trigger (via the internal accessor the module exposes for the active container
or by reading the mutable container reference on asyncData) and invoking
container._execute({ cause: 'watch', dedupe: options.dedupe }) only on that
resolved container.

@Flo0806 Flo0806 changed the title fix(useAsyncData): prevent duplicate execute on key change (#33274) fix(nuxt): prevent duplicate execute on key change (#33274) Sep 26, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Sep 26, 2025

CodSpeed Performance Report

Merging #33325 will not alter performance

Comparing Flo0806:useFetch-33274 (aa36a74) with main (bed410d)

Summary

✅ 10 untouched

@danielroe
Copy link
Member

thank you! ❤️

would you add a regression test in test/nuxt/use-async-data.test.ts? 🙏

@Flo0806
Copy link
Member Author

Flo0806 commented Sep 26, 2025

Sure!!
Tomorrow morning I'll be maked it.

@Flo0806
Copy link
Member Author

Flo0806 commented Sep 27, 2025

Hi! @danielroe! I've added the regression tests as requested 🙏

What the tests do:

  • Reproduce the exact bug scenario: external watch() defined before useAsyncData with a computed key
  • Verify that the fix prevents duplicate handler executions (3 calls instead of 4)
  • The extra execution was causing overlapping requests and "Request aborted" errors

Key test case:

// This watch before useAsyncData triggered the bug
watch(q, () => {})

const { data } = await useAsyncData(
  () => `query-${q.value}`, // Computed key
  () => handler(q.value),
  { watch: [q], immediate: true }
)

@danielroe danielroe changed the title fix(nuxt): prevent duplicate execute on key change (#33274) fix(nuxt): prevent duplicate execution on key change in useFetch Sep 27, 2025
@danielroe danielroe changed the title fix(nuxt): prevent duplicate execution on key change in useFetch fix(nuxt): prevent duplicate execution on key change in useAsyncData Sep 27, 2025
@danielroe danielroe merged commit 011c13e into nuxt:main Oct 2, 2025
47 checks passed
@github-actions github-actions bot mentioned this pull request Oct 2, 2025
@github-actions github-actions bot mentioned this pull request Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants