Fixes #31822: Surface domain tree search failures instead of rejecting unhandled - #31823
Fixes #31822: Surface domain tree search failures instead of rejecting unhandled#31823harsh-vador wants to merge 9 commits into
Conversation
`onSearch` in both domain trees awaited `searchDomains` inside a `try/finally` with no `catch`. The callback is async and is invoked by `debounce`, so nobody awaits it and a rejected request escaped as an unhandled promise rejection: no error toast, loader silently cleared, and a bare minified `AxiosError` in the browser error reporter. The sibling calls in the same component (`fetchAPI`, `loadChildDomains`) already toast, so the search path was the outlier. Also add the missing single-`|` entry to `ES_RESERVED_CHARACTERS`. The map is applied one character at a time, so its `'||'` key never matched and a lone pipe reached OpenSearch unescaped — `a||||b` becomes `q=*a||||b*`, consecutive OR operators, a `query_string` parse error, i.e. the 400 above. `'&'` was already mapped individually alongside `'&&'`; `'|'` was simply missing. Fixes #31822 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
✅ Playwright Results — workflow succeededValidated commit ✅ 553 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 46m 57s ⏱️ Max setup 4m 11s · max shard execution 14m 52s · max shard-job elapsed before upload 17m 54s · reporting 4s 🌐 215.67 requests/attempt · 2.83 app boots/UI scenario · 26.68% common-shard skew Optimization targets still in progress:
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
|
| Count | Rule |
|---|---|
| 13 | react-hooks/exhaustive-deps |
| 2 | jsx-a11y/click-events-have-key-events |
| 2 | jsx-a11y/no-static-element-interactions |
| 1 | jsx-a11y/no-autofocus |
| 1 | sonarjs/no-duplicate-string |
All findings
| Location | Rule | Message | |
|---|---|---|---|
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:405:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has an unnecessary dependency: 'initialDomains'. Either exclude it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:408:9 |
react-hooks/exhaustive-deps |
The 'onSelect' function makes the dependencies of useMemo Hook (at line 626) change on every render. Move it inside the useMemo callback. Alternatively, wrap th |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:427:9 |
react-hooks/exhaustive-deps |
The 'onCheck' function makes the dependencies of useMemo Hook (at line 626) change on every render. Move it inside the useMemo callback. Alternatively, wrap the |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:449:20 |
react-hooks/exhaustive-deps |
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:535:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has an unnecessary dependency: 'domains.length'. Either exclude it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:649:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'fetchAPI'. Either include it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:673:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has missing dependencies: 'loadChildDomains' and 'loadingChildren'. Either include them or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTree.tsx:686:9 |
jsx-a11y/no-autofocus |
The autoFocus prop should not be used, as it can reduce usability and accessibility for users. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:249:13 |
jsx-a11y/click-events-have-key-events |
Visible, non-interactive elements with click handlers must have at least one keyboard listener. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:249:13 |
jsx-a11y/no-static-element-interactions |
Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:303:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'buildIndex'. Either include it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:396:5 |
react-hooks/exhaustive-deps |
React Hook useCallback has an unnecessary dependency: 'initialDomains'. Either exclude it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:399:9 |
react-hooks/exhaustive-deps |
The 'onSelect' function makes the dependencies of useMemo Hook (at line 596) change on every render. Move it inside the useMemo callback. Alternatively, wrap th |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:417:9 |
react-hooks/exhaustive-deps |
The 'onCheck' function makes the dependencies of useMemo Hook (at line 596) change on every render. Move it inside the useMemo callback. Alternatively, wrap the |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:451:20 |
react-hooks/exhaustive-deps |
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:542:27 |
sonarjs/no-duplicate-string |
Define a constant instead of duplicating this literal 3 times. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:621:6 |
react-hooks/exhaustive-deps |
React Hook useEffect has a missing dependency: 'fetchAPI'. Either include it or remove the dependency array. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:691:7 |
jsx-a11y/click-events-have-key-events |
Visible, non-interactive elements with click handlers must have at least one keyboard listener. |
| 🟡 | src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx:691:7 |
jsx-a11y/no-static-element-interactions |
Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs |
Fix locally (fast - only checks files changed in this branch):
make ui-checkstyle-changed
Code Review ✅ Approved 2 resolved / 2 findingsSurfaces domain tree search failures inside the dropdown with a retry option rather than producing an unhandled rejection, but consider sanitizing the raw server error message before displaying it to users. ✅ 2 resolved✅ Quality: Search failures are now fully swallowed with no logging
✅ Security: Raw server error message surfaced directly in domain dropdown
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|
|
Filed the server-side half separately: #31878.
Keeping it out of this PR: it is a backend change in both search managers plus Worth noting it also settles the open review point here. The suggestion was to show the server's message only for 4xx and stay generic for 5xx. Today the one message a user can act on ("your search text is malformed") arrives as a 500, so that gate would hide exactly the useful case. Once #31878 lands and parse errors are 400s, the gate costs nothing and becomes the right call. |



Describe your changes:
Fixes #31822
A failed domain search was indistinguishable from an empty one, and the rejection escaped the component.
onSearchinDomainSelectableTree.tsxandDomainSelectableTreeNew.tsxawaitedsearchDomainsinside atry { ... } finally { setIsLoading(false) }with nocatch. The callback isasyncand is invoked bylodash.debounce, so nothing awaits it — a rejected request became an unhandled promise rejection. The user-visible damage: the loader cleared,treeDatakept its previous value, and the list fell through to the empty branch, so the dropdown said "No Domains available" for a request that had in fact failed. No error, no way to retry the term still sitting in the box. The sibling calls in the same component,fetchAPIandloadChildDomains, both alreadycatch → showErrorToast, so the search path was the only outlier.Reproducible with no code changes: type
a AND OR binto any domain selector.escapeESReservedCharactersescapes reserved characters, not boolean keywords, so that reaches OpenSearch asq=*a AND OR b*, aquery_stringparse error — and the dropdown reports "No Domains available".I also added the missing single-
|entry toES_RESERVED_CHARACTERS. The map is applied one character at a time, so its'||'key never matched and a lone pipe reached OpenSearch unescaped.||is Lucene'sOR, soq=*a||b*searches*aORb*instead of the literal string typed.'&'was already mapped individually alongside'&&';'|'was simply missing. This is a correctness fix, not a crash fix — see the note at the bottom.Type of change:
High-level design:
N/A — small change.
DomainSelectableTree.tsx/DomainSelectableTreeNew.tsx—onSearchnow catches, sets ahasSearchErrorflag and clearstreeData. ThetreeContentmemo gains a branch betweenisLoadingand the empty state, so a failure can never again render as "No Domains available". The branch uses each file's own existing empty-state markup (Box/Typographyin the first, a plain div inNew) plus aTry Againthat re-runs the same term. The flag clears on the next search and infetchAPI, which runs when the box is emptied.utils/StringUtils.ts— add'|': String.raw|``.On the surface for the error (per review feedback from @chirag-madlani): the failure belongs in the list, not in a toast. The user is looking at the dropdown, so that is where both the error and the recovery go.
showErrorToaststays onfetchAPIandloadChildDomains— those are the initial and child loads, where there is no search term to retry.On the escaper change: Lucene's classic query parser accepts
\before any character and treats the pair as a literal (_ESCAPED_CHAR), so\|searches for a literal pipe. Same treatment'&'already gets. Only affects terms containing a pipe.Tests:
Use cases covered
Try Againre-runs the same term and recovers.OR.Unit tests
components/common/DomainSelectableTree/DomainSelectableTree.test.tsx— three tests: inline error shown (andshowErrorToastnot called), retry recovers, clearing the box clears the error. Verified all three fail without thecatchand pass with it.utils/StringUtils.test.ts— newescapeESReservedCharactersblock: all reserved characters, the single/consecutive-pipe cases, a plain term left untouched,undefined→''.escapeESReservedCharactersis shared, so I also ran every other suite that exercises it —TagClassBase,QuickLinkFormModal,useTestSuitesData,useTestCaseList,DomainTreeView,ExploreUtils:eslintclean on all changed files (0 errors; pre-existing warnings inDomainSelectableTreeNew.tsxuntouched).tsc --noEmit: identical error count before and after this branch.Backend integration tests
Ingestion integration tests
Playwright (UI) tests
Manual testing performed
/api/v1/search/queryand/api/v1/domains/hierarchydirectly with curl to establish the real status codes — table at the bottom.q=*a AND OR b*ondomain_search_indexfails with aquery_shard_exception, which is the reproduction path in the issue.*/api/v1/search/query*in DevTools → Network against the patched build.UI screen recording / screenshots:
No visual change on any success path. The changed surface is the failure branch: where the dropdown previously showed "No Domains available" for a failed search, it now shows
Error while fetching Domainswith aTry Againlink, in the same position and using the same markup as the existing empty state.Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.Bug fix
Correction to an earlier revision of this description
The first version of this PR body claimed the unescaped pipe caused an HTTP 400 on
/search/query, and framed the escaper change as the fix for it. That was wrong. Measured against a running 1.13 stack:q=*a||||b*,index=domain_search_indexq=*a AND OR b*(Lucene parse error)q=*,from=9995(past the 10k result window)index=nope_search_index/domains/hierarchy?fields=parent,bogusInvalid field name bogus/domains/hierarchy?offset=-1must be greater than or equal to 0/search/querysurfaces its failures as 500, not 400. So the two changes here are independent: the missingcatchis a real defect on any status code (500 parse errors, network drops, refresh races), and the missing|escape is a separate correctness gap that returns 200 today. I could not measure the pipe's effect on returned hits — the instance I tested had no indexed data.Greptile Summary
This PR surfaces failed domain searches as an inline, retryable error state and adds escaping for individual pipe characters in Elasticsearch query terms.
ES_RESERVED_CHARACTERSwith the missing single-pipe mapping.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Reviews (9): Last reviewed commit: "Merge branch 'main' into fix/domain-tree..." | Re-trigger Greptile