) {
- const { children, id, query, results, fetching, cursor } = props;
+ const { children, id, query, results, fetching, cursor, error } = props;
const language = useLanguage();
@@ -82,6 +83,32 @@ export const SearchResults = React.forwardRef(function SearchResults(
);
}
+ if (error) {
+ return (
+
+
{t(language, 'search_ask_error')}
+
+
+ );
+ }
const noResults = (
({ results: [], fetching: false });
+ error: boolean;
+ }>({ results: [], fetching: false, error: false });
const { assistants } = useAI();
const withAI = assistants.length > 0;
@@ -54,7 +55,7 @@ export function useSearchResults(props: {
}
if (!query) {
if (!withAI) {
- setResultsState({ results: [], fetching: false });
+ setResultsState({ results: [], fetching: false, error: false });
return;
}
@@ -64,11 +65,11 @@ export function useSearchResults(props: {
results,
`Cached recommended questions should be set for site-space ${siteSpaceId}`
);
- setResultsState({ results, fetching: false });
+ setResultsState({ results, fetching: false, error: false });
return;
}
- setResultsState({ results: [], fetching: false });
+ setResultsState({ results: [], fetching: false, error: false });
let cancelled = false;
@@ -102,7 +103,11 @@ export function useSearchResults(props: {
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);
if (!cancelled) {
- setResultsState({ results: [...recommendedQuestions], fetching: false });
+ setResultsState({
+ results: [...recommendedQuestions],
+ fetching: false,
+ error: false,
+ });
}
}
}, 100);
@@ -112,44 +117,55 @@ export function useSearchResults(props: {
clearTimeout(timeout);
};
}
- setResultsState((prev) => ({ results: prev.results, fetching: true }));
+ setResultsState((prev) => ({ results: prev.results, fetching: true, error: false }));
let cancelled = false;
const timeout = setTimeout(async () => {
- const results = await (() => {
- if (scope === 'all') {
- // Search all content on the site
- return searchAllSiteContent(query);
- }
- if (scope === 'default') {
- // Search the current section's variant + matched/default variant for other sections
- return searchCurrentSiteSpaceContent(query, siteSpaceId);
- }
- if (scope === 'extended') {
- // Search all variants of the current section
- return searchSpecificSiteSpaceContent(query, siteSpaceIds);
+ try {
+ const results = await (() => {
+ if (scope === 'all') {
+ // Search all content on the site
+ return searchAllSiteContent(query);
+ }
+ if (scope === 'default') {
+ // Search the current section's variant + matched/default variant for other sections
+ return searchCurrentSiteSpaceContent(query, siteSpaceId);
+ }
+ if (scope === 'extended') {
+ // Search all variants of the current section
+ return searchSpecificSiteSpaceContent(query, siteSpaceIds);
+ }
+ if (scope === 'current') {
+ // Search only the current section's current variant
+ return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
+ }
+ throw new Error(`Unhandled search scope: ${scope}`);
+ })();
+
+ if (cancelled) {
+ return;
}
- if (scope === 'current') {
- // Search only the current section's current variant
- return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
+
+ if (!results) {
+ // One time when this one returns undefined is when it cannot find the server action and returns the html from the page.
+ // In that case, we want to avoid being stuck in a loading state, but it is an error.
+ // We could potentially try to force reload the page here, but i'm not 100% sure it would be a better experience.
+ setResultsState({ results: [], fetching: false, error: true });
+ return;
}
- throw new Error(`Unhandled search scope: ${scope}`);
- })();
- if (cancelled) {
- return;
- }
+ setResultsState({ results, fetching: false, error: false });
- if (!results) {
- setResultsState({ results: [], fetching: false });
- return;
+ trackEvent({
+ type: 'search_type_query',
+ query,
+ });
+ } catch {
+ // If there is an error, we need to catch it to avoid infinite loading state.
+ if (cancelled) {
+ return;
+ }
+ setResultsState({ results: [], fetching: false, error: true });
}
-
- setResultsState({ results, fetching: false });
-
- trackEvent({
- type: 'search_type_query',
- query,
- });
}, 350);
return () => {
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index c7c7e52e05..caa19c5421 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -202,6 +202,20 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// Handle redirects
//
if ('redirect' in siteURLData) {
+ // When it is a server action, we cannot just return a redirect response as it may cause CORS issues on redirect.
+ // For these cases, we return a 303 response with an `X-Action-Redirect` header that the client can handle.
+ // This is what server actions do when returning a redirect response.
+ const isServerAction = request.headers.has('next-action') && request.method === 'POST';
+ const createRedirectResponse = (url: string) =>
+ isServerAction
+ ? new NextResponse(null, {
+ status: 303,
+ headers: {
+ 'X-Action-Redirect': `${url};push`,
+ 'Content-Security-Policy': getContentSecurityPolicy(),
+ },
+ })
+ : NextResponse.redirect(url);
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
console.log('redirect', siteURLData.redirect);
if (siteURLData.target === 'content') {
@@ -221,10 +235,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// as it might contain a VA token
contentRedirect.search = request.nextUrl.search;
- return NextResponse.redirect(contentRedirect);
+ return createRedirectResponse(contentRedirect.toString());
}
- return NextResponse.redirect(siteURLData.redirect);
+ return createRedirectResponse(siteURLData.redirect);
}
cookies.push(