Fix crash when integration script fails to render block (#3332)

This commit is contained in:
Samy Pessé
2025-06-16 20:06:33 +02:00
committed by GitHub
parent af98402655
commit 11a6511b7a
7 changed files with 129 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix crash when integration script fails to render block.
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-contentkit": patch
---
Add basic error handling when transitioning between states.
+29 -5
View File
@@ -47,11 +47,7 @@ export function getDataOrNull<T>(
return response.then((result) => getDataOrNull(result, ignoreErrors));
}
if (response.error) {
if (ignoreErrors.includes(response.error.code)) return null;
throw new DataFetcherError(response.error.message, response.error.code);
}
return response.data;
return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
}
/**
@@ -93,6 +89,34 @@ export async function wrapDataFetcherError<T>(
}
}
/**
* Ignore some data fetcher errors.
*/
export function ignoreDataFetcherErrors<T>(
response: DataFetcherResponse<T>,
ignoreErrors?: number[]
): DataFetcherResponse<T>;
export function ignoreDataFetcherErrors<T>(
response: Promise<DataFetcherResponse<T>>,
ignoreErrors?: number[]
): Promise<DataFetcherResponse<T>>;
export function ignoreDataFetcherErrors<T>(
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
ignoreErrors: number[] = [404]
): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> {
if (response instanceof Promise) {
return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors));
}
if (response.error) {
if (ignoreErrors.includes(response.error.code)) {
return response;
}
throw new DataFetcherError(response.error.message, response.error.code);
}
return response;
}
/**
* Get a data fetcher exposable error from a JS error.
*/
@@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
import type { BlockProps } from '../Block';
import './contentkit.css';
import { getDataOrNull } from '@v2/lib/data';
import { contentKitServerContext } from './contentkit';
import { fetchSafeIntegrationUI } from './render';
import { renderIntegrationUi } from './server-actions';
export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
@@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
throw new Error('integration block requires a content.spaceId');
}
const { dataFetcher } = context.contentContext;
const initialInput: RenderIntegrationUI = {
componentId: block.data.block,
props: block.data.props,
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
},
};
const initialOutput = await getDataOrNull(
dataFetcher.renderIntegrationUi({
integrationName: block.data.integration,
request: initialInput,
}),
const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
integrationName: block.data.integration,
request: initialInput,
});
// The API can respond with a 400 error if the integration is not installed
// and 404 if the integration is not found.
[404, 400]
);
if (!initialOutput || initialOutput.type === 'complete') {
if (initialResponse.error) {
if (initialResponse.error.code === 404) {
return null;
}
return (
<div className={tcls(style)}>
<pre>
Unexpected error with integration {block.data.integration}:{' '}
{initialResponse.error.message}
</pre>
</div>
);
}
const initialOutput = initialResponse.data;
if (initialOutput.type === 'complete') {
return null;
}
@@ -0,0 +1,31 @@
import type { RenderIntegrationUI } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import { ignoreDataFetcherErrors } from '@v2/lib/data';
/**
* Render an integration UI while ignoring some errors.
*/
export async function fetchSafeIntegrationUI(
context: GitBookBaseContext,
{
integrationName,
request,
}: {
integrationName: string;
request: RenderIntegrationUI;
}
) {
const output = await ignoreDataFetcherErrors(
context.dataFetcher.renderIntegrationUi({
integrationName,
request,
}),
// The API can respond with a 400 error if the integration is not installed
// and 404 if the integration is not found.
// The API can also respond with a 502 error if the integration is not generating a proper response.
[404, 400, 502]
);
return output;
}
@@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import type { RenderIntegrationUI } from '@gitbook/api';
import { ContentKitOutput } from '@gitbook/react-contentkit';
import { throwIfDataError } from '@v2/lib/data';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { contentKitServerContext } from './contentkit';
import { fetchSafeIntegrationUI } from './render';
/**
* Server action to render an integration UI request from <ContentKit />.
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
request: RenderIntegrationUI;
}) {
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const output = await fetchSafeIntegrationUI(serverAction, {
integrationName: renderContext.integrationName,
request,
});
const output = await throwIfDataError(
serverAction.dataFetcher.renderIntegrationUi({
integrationName: renderContext.integrationName,
request,
})
);
if (output.error) {
return {
error: output.error.message,
};
}
return {
children: <ContentKitOutput output={output} context={contentKitServerContext} />,
output: output,
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
output: output.data,
};
}
+26 -15
View File
@@ -38,10 +38,18 @@ export function ContentKit<RenderContext>(props: {
render: (input: {
renderContext: RenderContext;
request: RequestRenderIntegrationUI;
}) => Promise<{
children: React.ReactNode;
output: ContentKitRenderOutput;
}>;
}) => Promise<
| {
error?: undefined;
children: React.ReactNode;
output: ContentKitRenderOutput;
}
| {
error: string;
children?: undefined;
output?: undefined;
}
>;
/** Callback when an action is triggered */
onAction?: (action: ContentKitAction) => void;
/** Callback when the flow is completed */
@@ -98,17 +106,18 @@ export function ContentKit<RenderContext>(props: {
request: newInput,
});
const output = result.output;
if (output) {
if (output.type === 'complete') {
return onComplete?.(output.returnValue);
}
if (output.type === 'complete') {
return onComplete?.(output.returnValue);
setCurrent((prev) => ({
input: newInput,
children: result.children,
output: output,
state: prev.state,
}));
}
setCurrent((prev) => ({
input: newInput,
children: result.children,
output: output,
state: prev.state,
}));
},
[setCurrent, current, render, onComplete]
);
@@ -147,8 +156,10 @@ export function ContentKit<RenderContext>(props: {
renderContext,
request: modalInput,
});
if (result.output.type === 'element' || !result.output.type) {
if (
result.output &&
(result.output.type === 'element' || !result.output.type)
) {
setSubView({
mode: 'modal',
initialInput: modalInput,