mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-18 16:45:11 +00:00
Fix crash when integration script fails to render block (#3332)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix crash when integration script fails to render block.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/react-contentkit": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add basic error handling when transitioning between states.
|
||||||
@@ -47,11 +47,7 @@ export function getDataOrNull<T>(
|
|||||||
return response.then((result) => getDataOrNull(result, ignoreErrors));
|
return response.then((result) => getDataOrNull(result, ignoreErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.error) {
|
return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
|
||||||
if (ignoreErrors.includes(response.error.code)) return null;
|
|
||||||
throw new DataFetcherError(response.error.message, response.error.code);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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.
|
* 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 type { BlockProps } from '../Block';
|
||||||
import './contentkit.css';
|
import './contentkit.css';
|
||||||
import { getDataOrNull } from '@v2/lib/data';
|
|
||||||
import { contentKitServerContext } from './contentkit';
|
import { contentKitServerContext } from './contentkit';
|
||||||
|
import { fetchSafeIntegrationUI } from './render';
|
||||||
import { renderIntegrationUi } from './server-actions';
|
import { renderIntegrationUi } from './server-actions';
|
||||||
|
|
||||||
export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
|
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');
|
throw new Error('integration block requires a content.spaceId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dataFetcher } = context.contentContext;
|
|
||||||
|
|
||||||
const initialInput: RenderIntegrationUI = {
|
const initialInput: RenderIntegrationUI = {
|
||||||
componentId: block.data.block,
|
componentId: block.data.block,
|
||||||
props: block.data.props,
|
props: block.data.props,
|
||||||
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialOutput = await getDataOrNull(
|
const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
|
||||||
dataFetcher.renderIntegrationUi({
|
integrationName: block.data.integration,
|
||||||
integrationName: block.data.integration,
|
request: initialInput,
|
||||||
request: initialInput,
|
});
|
||||||
}),
|
|
||||||
|
|
||||||
// The API can respond with a 400 error if the integration is not installed
|
if (initialResponse.error) {
|
||||||
// and 404 if the integration is not found.
|
if (initialResponse.error.code === 404) {
|
||||||
[404, 400]
|
return null;
|
||||||
);
|
}
|
||||||
if (!initialOutput || initialOutput.type === 'complete') {
|
|
||||||
|
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;
|
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 { isV2 } from '@/lib/v2';
|
||||||
import type { RenderIntegrationUI } from '@gitbook/api';
|
import type { RenderIntegrationUI } from '@gitbook/api';
|
||||||
import { ContentKitOutput } from '@gitbook/react-contentkit';
|
import { ContentKitOutput } from '@gitbook/react-contentkit';
|
||||||
import { throwIfDataError } from '@v2/lib/data';
|
|
||||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||||
import { contentKitServerContext } from './contentkit';
|
import { contentKitServerContext } from './contentkit';
|
||||||
|
import { fetchSafeIntegrationUI } from './render';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server action to render an integration UI request from <ContentKit />.
|
* Server action to render an integration UI request from <ContentKit />.
|
||||||
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
|
|||||||
request: RenderIntegrationUI;
|
request: RenderIntegrationUI;
|
||||||
}) {
|
}) {
|
||||||
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||||
|
const output = await fetchSafeIntegrationUI(serverAction, {
|
||||||
|
integrationName: renderContext.integrationName,
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
|
||||||
const output = await throwIfDataError(
|
if (output.error) {
|
||||||
serverAction.dataFetcher.renderIntegrationUi({
|
return {
|
||||||
integrationName: renderContext.integrationName,
|
error: output.error.message,
|
||||||
request,
|
};
|
||||||
})
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
children: <ContentKitOutput output={output} context={contentKitServerContext} />,
|
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
|
||||||
output: output,
|
output: output.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,18 @@ export function ContentKit<RenderContext>(props: {
|
|||||||
render: (input: {
|
render: (input: {
|
||||||
renderContext: RenderContext;
|
renderContext: RenderContext;
|
||||||
request: RequestRenderIntegrationUI;
|
request: RequestRenderIntegrationUI;
|
||||||
}) => Promise<{
|
}) => Promise<
|
||||||
children: React.ReactNode;
|
| {
|
||||||
output: ContentKitRenderOutput;
|
error?: undefined;
|
||||||
}>;
|
children: React.ReactNode;
|
||||||
|
output: ContentKitRenderOutput;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: string;
|
||||||
|
children?: undefined;
|
||||||
|
output?: undefined;
|
||||||
|
}
|
||||||
|
>;
|
||||||
/** Callback when an action is triggered */
|
/** Callback when an action is triggered */
|
||||||
onAction?: (action: ContentKitAction) => void;
|
onAction?: (action: ContentKitAction) => void;
|
||||||
/** Callback when the flow is completed */
|
/** Callback when the flow is completed */
|
||||||
@@ -98,17 +106,18 @@ export function ContentKit<RenderContext>(props: {
|
|||||||
request: newInput,
|
request: newInput,
|
||||||
});
|
});
|
||||||
const output = result.output;
|
const output = result.output;
|
||||||
|
if (output) {
|
||||||
|
if (output.type === 'complete') {
|
||||||
|
return onComplete?.(output.returnValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (output.type === 'complete') {
|
setCurrent((prev) => ({
|
||||||
return onComplete?.(output.returnValue);
|
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]
|
[setCurrent, current, render, onComplete]
|
||||||
);
|
);
|
||||||
@@ -147,8 +156,10 @@ export function ContentKit<RenderContext>(props: {
|
|||||||
renderContext,
|
renderContext,
|
||||||
request: modalInput,
|
request: modalInput,
|
||||||
});
|
});
|
||||||
|
if (
|
||||||
if (result.output.type === 'element' || !result.output.type) {
|
result.output &&
|
||||||
|
(result.output.type === 'element' || !result.output.type)
|
||||||
|
) {
|
||||||
setSubView({
|
setSubView({
|
||||||
mode: 'modal',
|
mode: 'modal',
|
||||||
initialInput: modalInput,
|
initialInput: modalInput,
|
||||||
|
|||||||
Reference in New Issue
Block a user