Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b8d3cb7

Browse files
Improve search error handling and token redirection (#3732)
1 parent 8ce7322 commit b8d3cb7

File tree

4 files changed

+99
-41
lines changed

4 files changed

+99
-41
lines changed

‎packages/gitbook/src/components/Search/SearchContainer.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function SearchContainer(props: SearchContainerProps) {
181181
const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true;
182182

183183
const searchResultsId = `search-results-${React.useId()}`;
184-
const { results, fetching } = useSearchResults({
184+
const { results, fetching, error } = useSearchResults({
185185
disabled: !(state?.query || withAI),
186186
query: normalizedQuery,
187187
siteSpaceId,
@@ -232,6 +232,7 @@ export function SearchContainer(props: SearchContainerProps) {
232232
fetching={fetching}
233233
results={results}
234234
cursor={cursor}
235+
error={error}
235236
/>
236237
) : null}
237238
{showAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}

‎packages/gitbook/src/components/Search/SearchResults.tsx‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type Assistant, useAI } from '@/components/AI';
77
import { t, useLanguage } from '@/intl/client';
88
import { tcls } from '@/lib/tailwind';
99

10-
import { Loading } from '../primitives';
10+
import { Button,Loading } from '../primitives';
1111
import { SearchPageResultItem } from './SearchPageResultItem';
1212
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
1313
import { SearchSectionResultItem } from './SearchSectionResultItem';
@@ -36,10 +36,11 @@ export const SearchResults = React.forwardRef(function SearchResults(
3636
results: ResultType[];
3737
fetching: boolean;
3838
cursor: number | null;
39+
error: boolean;
3940
},
4041
ref: React.Ref<SearchResultsRef>
4142
) {
42-
const { children, id, query, results, fetching, cursor } = props;
43+
const { children, id, query, results, fetching, cursor, error } = props;
4344

4445
const language = useLanguage();
4546

@@ -82,6 +83,32 @@ export const SearchResults = React.forwardRef(function SearchResults(
8283
</div>
8384
);
8485
}
86+
if (error) {
87+
return (
88+
<div
89+
className={tcls(
90+
'flex',
91+
'flex-col',
92+
'items-center',
93+
'justify-center',
94+
'text-center',
95+
'py-8',
96+
'h-full',
97+
'gap-4'
98+
)}
99+
>
100+
<div>{t(language, 'search_ask_error')}</div>
101+
<Button
102+
variant="secondary"
103+
size="small"
104+
// We do a reload because in case of a new deployment, the action might have changed and it requires a full reload to work again.
105+
onClick={() => window.location.reload()}
106+
>
107+
{t(language, 'unexpected_error_retry')}
108+
</Button>
109+
</div>
110+
);
111+
}
85112

86113
const noResults = (
87114
<div

‎packages/gitbook/src/components/Search/useSearchResults.ts‎

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export function useSearchResults(props: {
4343
const [resultsState, setResultsState] = React.useState<{
4444
results: ResultType[];
4545
fetching: boolean;
46-
}>({ results: [], fetching: false });
46+
error: boolean;
47+
}>({ results: [], fetching: false, error: false });
4748

4849
const { assistants } = useAI();
4950
const withAI = assistants.length > 0;
@@ -54,7 +55,7 @@ export function useSearchResults(props: {
5455
}
5556
if (!query) {
5657
if (!withAI) {
57-
setResultsState({ results: [], fetching: false });
58+
setResultsState({ results: [], fetching: false,error: false });
5859
return;
5960
}
6061

@@ -64,11 +65,11 @@ export function useSearchResults(props: {
6465
results,
6566
`Cached recommended questions should be set for site-space ${siteSpaceId}`
6667
);
67-
setResultsState({ results, fetching: false });
68+
setResultsState({ results, fetching: false,error: false });
6869
return;
6970
}
7071

71-
setResultsState({ results: [], fetching: false });
72+
setResultsState({ results: [], fetching: false,error: false });
7273

7374
let cancelled = false;
7475

@@ -102,7 +103,11 @@ export function useSearchResults(props: {
102103
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);
103104

104105
if (!cancelled) {
105-
setResultsState({ results: [...recommendedQuestions], fetching: false });
106+
setResultsState({
107+
results: [...recommendedQuestions],
108+
fetching: false,
109+
error: false,
110+
});
106111
}
107112
}
108113
}, 100);
@@ -112,44 +117,55 @@ export function useSearchResults(props: {
112117
clearTimeout(timeout);
113118
};
114119
}
115-
setResultsState((prev) => ({ results: prev.results, fetching: true }));
120+
setResultsState((prev) => ({ results: prev.results, fetching: true,error: false }));
116121
let cancelled = false;
117122
const timeout = setTimeout(async () => {
118-
const results = await (() => {
119-
if (scope === 'all') {
120-
// Search all content on the site
121-
return searchAllSiteContent(query);
122-
}
123-
if (scope === 'default') {
124-
// Search the current section's variant + matched/default variant for other sections
125-
return searchCurrentSiteSpaceContent(query, siteSpaceId);
126-
}
127-
if (scope === 'extended') {
128-
// Search all variants of the current section
129-
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
123+
try {
124+
const results = await (() => {
125+
if (scope === 'all') {
126+
// Search all content on the site
127+
return searchAllSiteContent(query);
128+
}
129+
if (scope === 'default') {
130+
// Search the current section's variant + matched/default variant for other sections
131+
return searchCurrentSiteSpaceContent(query, siteSpaceId);
132+
}
133+
if (scope === 'extended') {
134+
// Search all variants of the current section
135+
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
136+
}
137+
if (scope === 'current') {
138+
// Search only the current section's current variant
139+
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
140+
}
141+
throw new Error(`Unhandled search scope: ${scope}`);
142+
})();
143+
144+
if (cancelled) {
145+
return;
130146
}
131-
if (scope === 'current') {
132-
// Search only the current section's current variant
133-
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
147+
148+
if (!results) {
149+
// One time when this one returns undefined is when it cannot find the server action and returns the html from the page.
150+
// In that case, we want to avoid being stuck in a loading state, but it is an error.
151+
// We could potentially try to force reload the page here, but i'm not 100% sure it would be a better experience.
152+
setResultsState({ results: [], fetching: false, error: true });
153+
return;
134154
}
135-
throw new Error(`Unhandled search scope: ${scope}`);
136-
})();
137155

138-
if (cancelled) {
139-
return;
140-
}
156+
setResultsState({ results, fetching: false, error: false });
141157

142-
if (!results) {
143-
setResultsState({ results: [], fetching: false });
144-
return;
158+
trackEvent({
159+
type: 'search_type_query',
160+
query,
161+
});
162+
} catch {
163+
// If there is an error, we need to catch it to avoid infinite loading state.
164+
if (cancelled) {
165+
return;
166+
}
167+
setResultsState({ results: [], fetching: false, error: true });
145168
}
146-
147-
setResultsState({ results, fetching: false });
148-
149-
trackEvent({
150-
type: 'search_type_query',
151-
query,
152-
});
153169
}, 350);
154170

155171
return () => {

‎packages/gitbook/src/middleware.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,20 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
202202
// Handle redirects
203203
//
204204
if ('redirect' in siteURLData) {
205+
// When it is a server action, we cannot just return a redirect response as it may cause CORS issues on redirect.
206+
// For these cases, we return a 303 response with an `X-Action-Redirect` header that the client can handle.
207+
// This is what server actions do when returning a redirect response.
208+
const isServerAction = request.headers.has('next-action') && request.method === 'POST';
209+
const createRedirectResponse = (url: string) =>
210+
isServerAction
211+
? new NextResponse(null, {
212+
status: 303,
213+
headers: {
214+
'X-Action-Redirect': `${url};push`,
215+
'Content-Security-Policy': getContentSecurityPolicy(),
216+
},
217+
})
218+
: NextResponse.redirect(url);
205219
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
206220
console.log('redirect', siteURLData.redirect);
207221
if (siteURLData.target === 'content') {
@@ -221,10 +235,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
221235
// as it might contain a VA token
222236
contentRedirect.search = request.nextUrl.search;
223237

224-
return NextResponse.redirect(contentRedirect);
238+
return createRedirectResponse(contentRedirect.toString());
225239
}
226240

227-
return NextResponse.redirect(siteURLData.redirect);
241+
return createRedirectResponse(siteURLData.redirect);
228242
}
229243

230244
cookies.push(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /