diff --git a/api/src/bundler/index.ts b/api/src/bundler/index.ts index bb280afb..02b14c6e 100644 --- a/api/src/bundler/index.ts +++ b/api/src/bundler/index.ts @@ -1,12 +1,31 @@ import parseConfig, { Config, defaultConfig } from '../utils/config'; import { getGitHubContents, getPullRequestMetadata } from '../utils/github'; import { bundle } from './mdx'; +import { escapeHtml } from '../utils/sanitize'; export class BundlerError extends Error { - constructor(public code: number, name: string, message: string) { + code: number; + links?: { title: string; url: string }[]; + + constructor({ + code, + name, + message, + cause, + links, + }: { + code: number; + name: string; + message: string; + cause?: string; + links?: { title: string; url: string }[]; + }) { super(message); + this.code = code; this.name = name; this.message = message; + this.cause = cause; + this.links = links; } } @@ -95,21 +114,29 @@ class Bundler { }); if (!metadata) { - throw new BundlerError( - 404, - ERROR_CODES.REPO_NOT_FOUND, - `The repository ${this.#source.owner}/${this.#source.repository} was not found.`, - ); + throw new BundlerError({ + code: 404, + name: ERROR_CODES.REPO_NOT_FOUND, + message: `The repository ${this.#source.owner}/${this.#source.repository} was not found.`, + }); } if (!metadata.md) { - throw new BundlerError( - 404, - ERROR_CODES.FILE_NOT_FOUND, - `The file "/docs/${this.#path}.mdx" or "/docs/${this.#path}/index.mdx" in repository ${ - this.#source.owner - }/${this.#source.repository} was not found.`, - ); + throw new BundlerError({ + code: 404, + name: ERROR_CODES.FILE_NOT_FOUND, + message: `The file "/docs/${this.#path}.mdx" or "/docs/${ + this.#path + }/index.mdx" in repository /${ + this.#source.owner + '/' + this.#source.repository + } was not found.`, + links: [ + { + title: 'Repository link', + url: `https://github.com/${this.#source.owner}/${this.#source.repository}`, + }, + ], + }); } this.#markdown = metadata.md; @@ -153,14 +180,22 @@ class Bundler { }; } catch (e) { console.error(e); - throw new BundlerError( - 500, - ERROR_CODES.BUNDLE_ERROR, - `Something went wrong while bundling the file. Are you sure the MDX is valid? ${ - // @ts-ignore - e?.message || '' - }`, - ); + // @ts-ignore + const message = escapeHtml(e?.message || ''); + throw new BundlerError({ + code: 500, + name: ERROR_CODES.BUNDLE_ERROR, + message: `Something went wrong while bundling the file /${metadata.path}.mdx. Are you sure the MDX is valid?`, + cause: message, + links: [ + { + title: `/${metadata.path}.mdx on GitHub`, + url: `https://github.com/${this.#source.owner}/${this.#source.repository}/blob/${ + this.#ref + }/${metadata.path}.mdx`, + }, + ], + }); } }; } diff --git a/api/src/res.ts b/api/src/res.ts index 0c2e99e8..e16f52f5 100644 --- a/api/src/res.ts +++ b/api/src/res.ts @@ -15,7 +15,11 @@ export function response( code: string, other: | { - error: string; + error: { + message: string; + cause?: string | unknown; + links?: { title: string; url: string }[]; + }; } | { data: T; diff --git a/api/src/routes/bundle.ts b/api/src/routes/bundle.ts index df913bac..75d2538f 100644 --- a/api/src/routes/bundle.ts +++ b/api/src/routes/bundle.ts @@ -32,7 +32,11 @@ export default async function bundle(req: Request, res: Response): Promise { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; diff --git a/website/src/bundle.ts b/website/src/bundle.ts index 1e0c66ac..629eeb15 100644 --- a/website/src/bundle.ts +++ b/website/src/bundle.ts @@ -78,9 +78,15 @@ const $GetBundleResponseSuccess = z.object({ ), }); +const $GetBundleResponseError = z.object({ + message: z.string().catch(''), + cause: z.string().optional(), + links: z.array(z.object({ title: z.string(), url: z.string() })).optional(), +}); + const $ApiError = z.object({ code: z.enum(['NOT_FOUND', 'BAD_REQUEST', 'REPO_NOT_FOUND', 'FILE_NOT_FOUND', 'BUNDLE_ERROR']), - error: z.string().catch(''), + error: $GetBundleResponseError, }); const $GetBundleResponse = z.union([ @@ -127,6 +133,7 @@ export type GetBundleResponseSuccess = z.infer export type GetPreviewRequest = z.infer; export type GetPreviewResponse = z.infer; export type GetPreviewResponseSuccess = z.infer; +export type GetBundleResponseError = z.infer; export type BundleConfig = z.infer; export type SidebarArray = z.infer; diff --git a/website/src/layouts/ErrorPage.astro b/website/src/layouts/ErrorPage.astro index 589cc3cc..e168cdf5 100644 --- a/website/src/layouts/ErrorPage.astro +++ b/website/src/layouts/ErrorPage.astro @@ -1,13 +1,17 @@ --- +import type { GetBundleResponseError } from 'src/bundle'; + type Props = { code: number; + errorName?: string; title?: string; - description?: string; href?: string; - extraInfo?: string; + description?: string; + cause?: GetBundleResponseError['cause']; + links?: GetBundleResponseError['links']; }; -const { code, title, description, href, extraInfo } = Astro.props; +const { code, title, description, href, errorName, cause, links } = Astro.props; ---
@@ -21,45 +25,81 @@ const { code, title, description, href, extraInfo } = Astro.props;
-

{code} error

-

- {title || 'Something went wrong.'} -

-

- {description || `Sorry, something went wrong during the request.`} +

+ {code} error +

+ {title || 'Something went wrong.'} +

+ + { + description && ( +
+ {description || `Sorry, something went wrong during the request.`} + {cause && ( +

+ Possible cause: + {errorName && ( + + {errorName} + + )} +

+                      
+                    
+

+ )} + {links && links.length > 0 && ( +
    + {links.map(({ title, url }) => { + return ( +
  • + + {title} + +
  • + ); + })} +
+ )} +
+ ) + } + +

- { - extraInfo && ( -

- {extraInfo} -

- ) - } -
+ - diff --git a/website/src/pages/[owner]/[repository]/[...path].astro b/website/src/pages/[owner]/[repository]/[...path].astro index 18a7e3c1..a8cd9be7 100644 --- a/website/src/pages/[owner]/[repository]/[...path].astro +++ b/website/src/pages/[owner]/[repository]/[...path].astro @@ -3,7 +3,7 @@ import Root from '@layouts/Root.astro'; import DocsView from '@layouts/DocsView.tsx'; import ErrorPage from '@layouts/ErrorPage.astro'; -import { type GetBundleResponse, getBundle } from 'src/bundle'; +import { type GetBundleResponse, getBundle, type GetBundleResponseError } from 'src/bundle'; import { trackPageRequest } from 'src/plausible'; import context from 'src/context'; import domains from '../../../../../domains.json'; @@ -30,7 +30,7 @@ if (repository.includes('~')) { let response: GetBundleResponse | undefined; let code: GetBundleResponse['code']; -let message: string = ''; +let error: GetBundleResponseError = { message: '' }; try { response = await getBundle({ @@ -41,11 +41,12 @@ try { }); code = response.code; - if ('error' in response) message = response.error; + // refer to [api/src/routes/bundle.ts] for more information; + if ('error' in response) error = response.error; } catch (e) { console.error(e); code = 'BUNDLE_ERROR'; - message = 'An error occurred while fetching the bundle.' + error.message = 'An error occurred while fetching the bundle.' } const statusCodes = { @@ -153,6 +154,7 @@ if (response?.code === 'OK') { theme: theme ? (theme === 'dark' ? 'dark' : 'light') : undefined, tabs, }); + } function codeToTitle(code: GetBundleResponse['code']) { @@ -174,10 +176,11 @@ function codeToTitle(code: GetBundleResponse['code']) { ) @@ -189,9 +192,10 @@ function codeToTitle(code: GetBundleResponse['code']) { )