Skip to content

Commit 0d5d127

Browse files
Merge pull request #199 from RtlZeroMemory/guardrails/router-guards
fix(core): harden router guard error handling and param validation
2 parents f03d99d + 40af6d3 commit 0d5d127

File tree

2 files changed

+41
-10
lines changed

2 files changed

+41
-10
lines changed

packages/core/src/router/integration.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,23 @@ export function createRouterIntegration<S>(
157157
const guard = ancestryRoute.guard;
158158
if (!guard) continue;
159159

160-
const guardResult = guard(
161-
resolvedParams,
162-
opts.getState(),
163-
Object.freeze({
164-
from,
165-
to,
166-
action,
167-
}),
168-
);
160+
let guardResult: boolean | { redirect: string; params?: RouteParams };
161+
try {
162+
guardResult = guard(
163+
resolvedParams,
164+
opts.getState(),
165+
Object.freeze({
166+
from,
167+
to,
168+
action,
169+
}),
170+
);
171+
} catch (e) {
172+
throw new ZrUiError(
173+
"ZRUI_USER_CODE_THROW",
174+
`route guard for "${ancestryRouteId}" threw: ${e instanceof Error ? `${e.name}: ${e.message}` : String(e)}`,
175+
);
176+
}
169177

170178
if (guardResult === true) {
171179
continue;

packages/core/src/router/router.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import type {
88
RouterStateSnapshot,
99
} from "./types.js";
1010

11+
const NODE_ENV =
12+
(globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV ??
13+
"development";
14+
const DEV_MODE = NODE_ENV !== "production";
15+
16+
function warnDev(message: string): void {
17+
if (!DEV_MODE) return;
18+
const c = (globalThis as { console?: { warn?: (msg: string) => void } }).console;
19+
c?.warn?.(message);
20+
}
21+
1122
const DEFAULT_HISTORY_DEPTH = 50;
1223

1324
export type RouteRecord<S> = Readonly<{
@@ -30,6 +41,11 @@ function normalizeRouteId(routeId: string): string {
3041
if (!normalized) {
3142
throwInvalidProps("route id must be a non-empty string");
3243
}
44+
if (DEV_MODE && /[^a-zA-Z0-9_\-.]/.test(normalized)) {
45+
warnDev(
46+
`[rezi] route id "${normalized}" contains non-identifier characters. Use only letters, digits, hyphens, underscores, and dots.`,
47+
);
48+
}
3349
return normalized;
3450
}
3551

@@ -40,7 +56,14 @@ export function normalizeRouteParams(params: RouteParams | undefined): RoutePara
4056
if (!params) return Object.freeze({});
4157

4258
const entries = Object.entries(params)
43-
.map(([key, value]) => [String(key), String(value)] as const)
59+
.map(([key, value]) => {
60+
if (DEV_MODE && typeof value !== "string") {
61+
warnDev(
62+
`[rezi] route param "${key}" has non-string value (${typeof value}), coerced to string. Pass string values to avoid implicit coercion.`,
63+
);
64+
}
65+
return [String(key), String(value)] as const;
66+
})
4467
.sort((a, b) => {
4568
if (a[0] < b[0]) return -1;
4669
if (a[0] > b[0]) return 1;

0 commit comments

Comments
 (0)