Skip to content

fix: prevent search results from being cleared on task cancellation#2253

Open
tiran133 wants to merge 1 commit intoopencloud-eu:mainfrom
tiran133:fix-search
Open

fix: prevent search results from being cleared on task cancellation#2253
tiran133 wants to merge 1 commit intoopencloud-eu:mainfrom
tiran133:fix-search

Conversation

@tiran133
Copy link
Copy Markdown

@tiran133 tiran133 commented Mar 28, 2026

Disclaimer: This bug was discovered by myself and fixed with the help of AI. This pull request description was also written by AI and reviewd by me. The fix was confirmed by myself using test methods below.

Description

Fix a race condition where search results display "No results found" even though the search request returns valid results. This happens when the user is already on the search results page and performs a new search.

Two bugs are fixed:

  1. Stale route name in Preview.available (web-app-files/src/search/sdk/preview.ts): The Preview.available getter checks against the route name 'search-provider-list', which does not exist in the codebase. The actual search results route is 'files-common-search'. Because of this, Preview.available always returns true — even when the user is on the search results page — so the SearchBar's preview search is never disabled on that page.

  2. No cancellation handling in list search (web-app-search/src/views/List.vue): Both the list search and the preview search share the same searchTask (created via useSearch() in extensions.ts), which uses vue-concurrency's .restartable() modifier. When the preview search fires (due to Bug 1 not disabling it on the search page), it cancels the in-flight list search. The catch block in List.vue does not distinguish cancellation errors from real errors, so it resets searchResult to empty and sets loading to false — overwriting valid results or preventing the UI from showing them.

Race condition timeline (when already on the search page):

  1. Route query changes → List.vue emits 'search'searchTask.perform(term, 200)Request A starts
  2. SearchBar.vue's $route watcher fires parseRouteQuery(route, false) → sets restoreSearchFromRoute = falseterm watcher calls debouncedSearch() (500ms debounce)
  3. ~500ms later: debouncedSearch fires → previewSearch.search()searchTask.perform(term, 8)Request B starts, cancels Request A via .restartable()
  4. Request A rejects with the string 'cancel'catch block resets searchResult = { values: [], totalResults: null }, loading = false"No results found"
  5. Request B resolves → updates SearchBar's preview dropdown, but not the list page's searchResult ref

Related Issue

If you want me to create an Github Issue first, let me know.

Motivation and Context

When using OpenCloud and performing a search while already on the search results page causes the results to be cleared. The user sees "No results found" even though the network request eventually returns valid results. This is confusing and breaks the search experience, particularly for extensions that navigate to the built-in search page.

How Has This Been Tested?

  • test environment: Chrome DevTools Network tab with network throttling enabled (e.g. "Slow 3G" or "Fast 3G") to reliably reproduce the race condition. The bug requires slow network conditions so that Request A is still in-flight when the SearchBar's debounced preview search fires ~500ms later and cancels it.
  • test case 1: Enable network throttling in DevTools, navigate to search results page, then change the search term — verify results are displayed correctly instead of "No results found"
  • test case 2: Enable network throttling, perform a search from a non-search page (first navigation) — verify behavior is unchanged and results display correctly
  • test case 3: Enable network throttling, rapidly change search terms while on the search results page — verify no stale/empty results are shown
  • test case 4: Disable network throttling and repeat the above — verify everything still works correctly under normal network conditions

Screenshots (if appropriate):

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Technical debt
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Documentation added

Fix stale route name 'search-provider-list' to 'files-common-search'
in Preview.available so preview search is correctly disabled on the
search results page. Add cancellation handling in List.vue catch block
to avoid resetting searchResult when vue-concurrency's restartable
task throws 'cancel'. Update unit test to use correct route name.
Copy link
Copy Markdown
Member

@JammingBen JammingBen left a comment

Choose a reason for hiding this comment

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

This is definitely an issue, thanks for bringing it up! I'm not quite happy with the solution though.

search-provider-list seems to be a stale route indeed, but I think the search preview should be available on the search result page. It doesn't only show file search results, it's also an extension point for other apps (e.g. https://github.com/opencloud-eu/web-extensions/tree/main/packages/web-app-calculator, which throws errors on the search result page with this change). Also, we want to extend its core functionality in the future to reveal more than just files.

You could try your fix after removing this condition, but it will probably break things again. IMO the real issue is that both the preview and the list use the same task instance for loading search results.

@tiran133
Copy link
Copy Markdown
Author

Agree it's not ideal.
Not sure, though, if the search preview really needs to be on the search page.
I guess that's a UX decision.

Not sure what to do about this one then. There is a test is not available on certain routes.
So I guess this was intentional at some point? Don't know the history.

This would then be changed to this and change the the to is available on all routes

public get available(): boolean {
return (
unref(this.router.currentRoute).name !== 'search-provider-list' &&
!this.configStore.options?.embed?.enabled
)

  public get available(): boolean {
    return true;
 }

Proposed solution:
Without rework of useSearch tasks by cancelling debounced search

Let me know what you think, and I can push that so you can try it for yourself.

packages/web-app-search/src/portals/SearchBar.vue

diff --git a/packages/web-app-search/src/portals/SearchBar.vue b/packages/web-app-search/src/portals/SearchBar.vue
--- a/packages/web-app-search/src/portals/SearchBar.vue	(revision 2ff99ae77ed67012f64cca3cf9bd987420f9233c)
+++ b/packages/web-app-search/src/portals/SearchBar.vue	(date 1774909326974)
@@ -398,7 +398,8 @@
       getSearchResultLocation,
       showDrop,
       isAppActive,
-      getFocusableElements
+      getFocusableElements,
+      debouncedSearch,
     }
   },
 
@@ -503,6 +504,10 @@
         this.currentFolderAvailable = currentFolderAvailable
       }
 
+      if (isLocationCommonActive(this.$router, 'files-common-search')) {
+        this.debouncedSearch.cancel()
+      }
+
       this.$nextTick(() => {
         if (!this.availableProviders.length) {
           return

packages/web-app-files/src/search/sdk/preview.ts

diff --git a/packages/web-app-files/src/search/sdk/preview.ts b/packages/web-app-files/src/search/sdk/preview.ts
--- a/packages/web-app-files/src/search/sdk/preview.ts	(revision 2ff99ae77ed67012f64cca3cf9bd987420f9233c)
+++ b/packages/web-app-files/src/search/sdk/preview.ts	(date 1774907102967)
@@ -23,9 +23,6 @@
   }
 
   public get available(): boolean {
-    return (
-      unref(this.router.currentRoute).name !== 'files-common-search' &&
-      !this.configStore.options?.embed?.enabled
-    )
+    return true
   }
 }

packages/web-app-files/tests/unit/search/sdk.spec.ts

diff --git a/packages/web-app-files/tests/unit/search/sdk.spec.ts b/packages/web-app-files/tests/unit/search/sdk.spec.ts
--- a/packages/web-app-files/tests/unit/search/sdk.spec.ts	(revision 2ff99ae77ed67012f64cca3cf9bd987420f9233c)
+++ b/packages/web-app-files/tests/unit/search/sdk.spec.ts	(date 1774906483526)
@@ -19,10 +19,10 @@
   })
 
   describe('SDKProvider previewSearch', () => {
-    it('is not available on certain routes', () => {
+    it('is available on all routes', () => {
       ;[
         { route: 'foo', available: true },
-        { route: 'files-common-search' },
+        { route: 'files-common-search', available: true },
         { route: 'bar', available: true }
       ].forEach((v) => {
         const router = mock<Router>()
@@ -35,7 +35,7 @@
           mock<ConfigStore>()
         )
 
-        expect(!!search.previewSearch.available).toBe(!!v.available)
+        expect(search.previewSearch.available).toBe(true)
       })
     })
   })

@tiran133
Copy link
Copy Markdown
Author

tiran133 commented Mar 31, 2026

Just realized that it might be even better to cancel the debounce in onKeyUpEnter

But neither is handling the case properly. When you do the following

  • You search and hit enter.
  • While the search is running, type something in the search box, but do not press enter.
  • Preview returns and gets displayed.
  • Search page stuck in loading state/ No results, depending on if you include the if statement in my pull request.

That's when you mention that both searches share the same task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants