Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 03f431f

Browse files
committed
Improve error handling
1 parent be518a6 commit 03f431f

File tree

6 files changed

+106
-67
lines changed

6 files changed

+106
-67
lines changed

framework/react/data.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ export const useData = <T = unknown>(): {
2020
const [data, setData] = useState(() => {
2121
const cached = dataCache.get(dataUrl);
2222
if (cached) {
23+
if (cached.data instanceof Error) {
24+
throw cached.data;
25+
}
2326
if (typeof cached.data === "function") {
2427
const data = cached.data();
2528
if (data instanceof Promise) {
2629
throw data.then((data) => {
2730
cached.data = data;
28-
return data;
31+
}).catch((error) => {
32+
cached.data = error;
2933
});
3034
}
3135
throw new Error(`Data for ${dataUrl} has invalid type [function].`);

framework/react/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, { error: Error
1414
}
1515

1616
render() {
17-
if (this.state.error) {
17+
if (this.state.error instanceof Error) {
1818
return createElement(this.props.Handler, { error: this.state.error });
1919
}
2020

framework/react/router.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,29 +51,36 @@ export const Router: FC<RouterProps> = ({ ssrContext, suspense }) => {
5151
const { url, defaultExport } = modules[0];
5252
const dataUrl = url.pathname + url.search;
5353
const el = createElement(
54-
DataContext.Provider,
54+
ErrorBoundary,
5555
{
56-
value: {
57-
dataUrl,
58-
dataCache,
59-
ssrHeadCollection: ssrContext?.headCollection,
60-
},
61-
key: dataUrl,
56+
Handler: ErrorBoundaryHandler || (({ error }: { error: Error }) =>
57+
createElement(Err, {
58+
status: 500,
59+
statusText: error.message,
60+
})),
6261
},
63-
typeof defaultExport === "function"
64-
? createElement(
65-
defaultExport as FC,
66-
null,
67-
modules.length > 1 ? createRouteEl(modules.slice(1)) : undefined,
68-
)
69-
: createElement(Err, {
70-
status: 400,
71-
statusText: "missing default export as a valid React component",
72-
}),
62+
createElement(
63+
DataContext.Provider,
64+
{
65+
value: {
66+
dataUrl,
67+
dataCache,
68+
ssrHeadCollection: ssrContext?.headCollection,
69+
},
70+
key: dataUrl,
71+
},
72+
typeof defaultExport === "function"
73+
? createElement(
74+
defaultExport as FC,
75+
null,
76+
modules.length > 1 ? createRouteEl(modules.slice(1)) : undefined,
77+
)
78+
: createElement(Err, {
79+
status: 400,
80+
statusText: "missing default export as a valid React component",
81+
}),
82+
),
7383
);
74-
if (ErrorBoundaryHandler) {
75-
return createElement(ErrorBoundary, { Handler: ErrorBoundaryHandler }, el);
76-
}
7784
return el;
7885
};
7986
const routeEl = useMemo(() => {

server/mod.ts

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import renderer from "./renderer.ts";
1212
import { content, type CookieOptions, json, setCookieHeader } from "./response.ts";
1313
import { importRouteModule, initRoutes, revive } from "./routing.ts";
1414
import clientModuleTransformer from "./transformer.ts";
15-
import type { AlephConfig, FetchHandler, Middleware, MiddlewareCallback } from "./types.ts";
15+
import type { AlephConfig, FetchHandler, Middleware } from "./types.ts";
1616

1717
export type ServerOptions = ServeInit & {
1818
certFile?: string;
@@ -23,10 +23,11 @@ export type ServerOptions = ServeInit & {
2323
middlewares?: Middleware[];
2424
fetch?: FetchHandler;
2525
ssr?: SSR;
26+
onError?(error: unknown): Promise<Response> | Response;
2627
};
2728

2829
export const serve = (options: ServerOptions = {}) => {
29-
const { config, middlewares, fetch, ssr, logLevel } = options;
30+
const { config, middlewares, fetch, ssr, logLevel, onError } = options;
3031
const isDev = Deno.env.get("ALEPH_ENV") === "development";
3132
const importMapPromise = loadImportMap();
3233
const jsxConfigPromise = importMapPromise.then((importMap) => loadJSXConfig(importMap));
@@ -50,32 +51,39 @@ export const serve = (options: ServerOptions = {}) => {
5051

5152
// transform client modules
5253
if (clientModuleTransformer.test(pathname)) {
53-
const [buildHash, jsxConfig, importMap] = await Promise.all([
54-
buildHashPromise,
55-
jsxConfigPromise,
56-
importMapPromise,
57-
]);
58-
return clientModuleTransformer.fetch(req, {
59-
importMap,
60-
jsxConfig,
61-
buildHash,
62-
buildTarget: config?.build?.target,
63-
isDev,
64-
});
54+
try {
55+
const [buildHash, jsxConfig, importMap] = await Promise.all([
56+
buildHashPromise,
57+
jsxConfigPromise,
58+
importMapPromise,
59+
]);
60+
return await clientModuleTransformer.fetch(req, {
61+
importMap,
62+
jsxConfig,
63+
buildHash,
64+
buildTarget: config?.build?.target,
65+
isDev,
66+
});
67+
} catch (err) {
68+
if (!(err instanceof Deno.errors.NotFound)) {
69+
log.error(err);
70+
return onError?.(err) ?? new Response(err.message, { status: 500 });
71+
}
72+
}
6573
}
6674

6775
// use loader to load modules
6876
const moduleLoaders = await moduleLoadersPromise;
6977
const loader = moduleLoaders.find((loader) => loader.test(pathname));
7078
if (loader) {
71-
const [buildHash, jsxConfig, importMap] = await Promise.all([
72-
buildHashPromise,
73-
jsxConfigPromise,
74-
importMapPromise,
75-
]);
7679
try {
80+
const [buildHash, jsxConfig, importMap] = await Promise.all([
81+
buildHashPromise,
82+
jsxConfigPromise,
83+
importMapPromise,
84+
]);
7785
const loaded = await loader.load(pathname, { isDev, importMap });
78-
return clientModuleTransformer.fetch(req, {
86+
return await clientModuleTransformer.fetch(req, {
7987
loaded,
8088
importMap,
8189
jsxConfig,
@@ -86,7 +94,7 @@ export const serve = (options: ServerOptions = {}) => {
8694
} catch (err) {
8795
if (!(err instanceof Deno.errors.NotFound)) {
8896
log.error(err);
89-
return new Response(err.message, { status: 500 });
97+
return onError?.(err) ?? new Response(err.message, { status: 500 });
9098
}
9199
}
92100
}
@@ -123,7 +131,7 @@ export const serve = (options: ServerOptions = {}) => {
123131
} catch (err) {
124132
if (!(err instanceof Deno.errors.NotFound)) {
125133
log.error(err);
126-
return new Response("Internal Server Error", { status: 500 });
134+
return onError?.(err) ?? new Response(err.message, { status: 500 });
127135
}
128136
}
129137
}
@@ -205,24 +213,24 @@ export const serve = (options: ServerOptions = {}) => {
205213

206214
// use middlewares
207215
if (Array.isArray(middlewares) && middlewares.length > 0) {
208-
const callbacks: MiddlewareCallback[] = [];
209-
for (const mw of middlewares) {
210-
const handler = mw.fetch;
211-
if (typeof handler === "function") {
212-
let res = handler(req, ctx);
213-
if (res instanceof Promise) {
214-
res = await res;
215-
}
216-
if (res instanceof Response) {
217-
return res;
218-
}
219-
if (typeof res === "function") {
220-
callbacks.push(res);
216+
try {
217+
for (const mw of middlewares) {
218+
const handler = mw.fetch;
219+
if (typeof handler === "function") {
220+
let res = handler(req, ctx);
221+
if (res instanceof Promise) {
222+
res = await res;
223+
}
224+
if (res instanceof Response) {
225+
return res;
226+
}
227+
if (typeof res === "function") {
228+
setTimeout(res, 0);
229+
}
221230
}
222231
}
223-
}
224-
for (const callback of callbacks) {
225-
await callback();
232+
} catch (err) {
233+
return onError?.(err) ?? new Response(err.message, { status: 500 });
226234
}
227235
}
228236

@@ -241,7 +249,23 @@ export const serve = (options: ServerOptions = {}) => {
241249
) {
242250
const fetcher = dataConfig[req.method.toLowerCase()];
243251
if (typeof fetcher === "function") {
244-
return fetcher(req, { ...ctx, params: ret.pathname.groups });
252+
const res = await fetcher(req, { ...ctx, params: ret.pathname.groups });
253+
console.log(res);
254+
if (res instanceof Response) {
255+
return res;
256+
}
257+
if (
258+
typeof res === "string" || res instanceof ArrayBuffer || res instanceof ReadableStream
259+
) {
260+
return new Response(res);
261+
}
262+
if (res instanceof Blob) {
263+
return new Response(res, { headers: { "Content-Type": res.type } });
264+
}
265+
if (util.isPlainObject(res) || Array.isArray(res) || res === null) {
266+
return json(res);
267+
}
268+
return new Response(null);
245269
}
246270
return new Response("Method not allowed", { status: 405 });
247271
}
@@ -277,7 +301,7 @@ export const serve = (options: ServerOptions = {}) => {
277301
indexHtml = null;
278302
} else {
279303
log.error("read index.html:", err);
280-
return new Response("Internal Server Error", { status: 500 });
304+
return onError?.(err) ?? new Response(err.message, { status: 500 });
281305
}
282306
}
283307
}

server/renderer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type SSRContext = {
1717
readonly errorBoundaryHandler?: CallableFunction;
1818
readonly signal: AbortSignal;
1919
readonly bootstrapScripts?: string[];
20+
readonly onError?: (error: unknown) => void;
2021
};
2122

2223
export type HTMLRewriterHandlers = {
@@ -61,14 +62,17 @@ export default {
6162
const render = typeof ssr === "function" ? ssr : ssr.render;
6263
try {
6364
const headCollection: string[] = [];
64-
const ssrContext = {
65+
const ssrContext: SSRContext = {
6566
url,
6667
routeModules,
67-
errorBoundaryHandler: errorBoundaryHandler?.default,
6868
headCollection,
6969
suspense,
70+
errorBoundaryHandler: errorBoundaryHandler?.default,
7071
signal: req.signal,
7172
bootstrapScripts: [bootstrapScript],
73+
onError: (_error: unknown) => {
74+
// todo: handle suspense error
75+
},
7276
};
7377
const body = await render(ssrContext);
7478
const serverDependencyGraph: DependencyGraph | undefined = Reflect.get(globalThis, "serverDependencyGraph");
@@ -150,7 +154,7 @@ export default {
150154
}
151155
return line;
152156
}).join("\n");
153-
log.error(e);
157+
log.error("SSR", e);
154158
} else {
155159
message = e?.toString?.() || String(e);
156160
}
@@ -370,7 +374,7 @@ async function initSSR(
370374
throw new FetchError(500, {}, "Data must be valid JSON");
371375
}
372376
} else {
373-
throw new Error("Data response must be a JSON");
377+
throw new FetchError(500, {}, "Data must be valid JSON");
374378
}
375379
};
376380
if (suspense) {

server/transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default {
9292
const alephPkgUri = getAlephPkgUri();
9393
const { jsxConfig, importMap, buildTarget } = options;
9494
let ret: TransformResult;
95-
if (/^https?:\/\/(cdn\.)esm\.sh\//.test(specifier)) {
95+
if (/^https?:\/\/((cdn\.)?esm\.sh|unpkg\.com)\//.test(specifier)) {
9696
// don't transform modules imported from esm.sh
9797
const deps = await parseDeps(specifier, sourceCode, { importMap: JSON.stringify(importMap) });
9898
if (deps.length > 0) {

0 commit comments

Comments
 (0)