Recovery Hydration (client-error.jsx)
Understand how Dinou bootstraps client-side error recovery using POST payload intercepts, allowing developers to inspect trace overlays without losing SPA transition contexts.
Key File Location: ./dinou/core/client-error.jsx💡 Overview
The client-error.jsx entrypoint targets the compilation of the error.js client script. When the server crashes during initial HTML rendering, it streams fallback error layouts, registers exception details, and registers the error.js file as the hydration runtime.
📊 Physical File Structure
The module layout structures error-initialization flags and intercept routines:
🔗 1. Imports & Core Modules
The module imports core modules identical to the standard hydration client:
import {
use,
useState,
useEffect,
useTransition,
useLayoutEffect,
useMemo,
Component,
} from "react";
import { createFromFetch } from "@roggc/react-server-dom-esm/client";
import { hydrateRoot } from "react-dom/client";
import { RouterContext } from "./navigation.js";
import { resolveUrl, isExternalUrl } from "./navigation-utils.js";
import { createServerFunctionProxy } from "./server-function-proxy.js";💾 2. Global Module State
In addition to cache and scrollCache maps, the global scope defines an error gate:
isInitialErrorLoad: A boolean flag initialized totrue. It ensures that the initial request payload is intercepted and processed as a POST request to send server-rendered crash data.
const cache = new Map();
const scrollCache = new Map();
const getCurrentRoute = () => window.location.pathname + window.location.search;
// CRITICAL FLAG: Identifies first render hydration after a crash
let isInitialErrorLoad = true;⚙️ 3. Pure Helpers & Error Hydration
The getRSCPayload() function is customized to intercept initial loading on crashed routes:
- The POST Interception: If
isInitialErrorLoadis true and the target path equals the current URL, the function immediately setsisInitialErrorLoad = false. Instead of a normal GET request, it fires an HTTP POST request to/____rsc_payload_error____, packaging the error details from global window states (window.__DINOU_ERROR_MESSAGE__, etc.) into the request body. - Standard GET Fallback: If the user navigates away by clicking a links or history popstates, the flag evaluates to
false. Subsequent network operations resolve using standard GET paths to/____rsc_payload____, restoring smooth SPA flows.
const isHashChangeOnly = (finalPath) => {
const targetUrl = new URL(finalPath, window.location.origin);
const normalize = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
const targetPath = normalize(targetUrl.pathname);
const currentPath = normalize(window.location.pathname);
return (
targetPath + targetUrl.search === currentPath + window.location.search &&
targetUrl.hash !== ""
);
};
const getRSCPayload = (rscKey, isPrefetch = false) => {
const url = rscKey.split("::")[0];
if (cache.has(url)) return cache.get(url);
let promise;
// --- ⚠️ Initial Load POST Interception
if (isInitialErrorLoad && url === getCurrentRoute()) {
isInitialErrorLoad = false;
const payloadUrl = "/____rsc_payload_error____" + url;
promise = createFromFetch(
fetch(payloadUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
error: {
message: window.__DINOU_ERROR_MESSAGE__ || "Unknown Error",
stack: window.__DINOU_ERROR_STACK__,
name: window.__DINOU_ERROR_NAME__,
},
}),
}).then((res) => {
if (res.headers.has("x-rsc-redirect")) {
const redirectUrl = res.headers.get("x-rsc-redirect");
cache.delete(url);
if (!isPrefetch) {
if (window.__DINOU_ROUTER_NAVIGATE__) {
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
} else {
window.location.href = redirectUrl;
}
}
return new Promise(() => {});
}
return res;
}),
{
callServer: async (id, args) => createServerFunctionProxy(id)(...args)
}
);
} else {
// --- 🟢 Recovery Complete: Fallback to standard GET requests on navigation
let payloadUrl = "/____rsc_payload____" + url;
const buildId = window.__DINOU_BUILD_ID__;
if (buildId) {
payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
window.__DINOU_BUILD_ID__ = undefined;
}
promise = createFromFetch(
fetch(payloadUrl).then((res) => {
if (res.headers.has("x-rsc-redirect")) {
const redirectUrl = res.headers.get("x-rsc-redirect");
cache.delete(url);
if (!isPrefetch) {
if (window.__DINOU_ROUTER_NAVIGATE__) {
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
} else {
window.location.href = redirectUrl;
}
}
return new Promise(() => {});
}
return res;
}),
{
callServer: async (id, args) => createServerFunctionProxy(id)(...args)
}
);
}
cache.set(url, promise);
return promise;
};🚏 4. The Router Component
Bootstraps the application similarly to the primary router, mounting custom contexts and hydration hooks:
// Structure matches client.jsx Router component:
function Router() {
const [route, setRoute] = useState(getCurrentRoute());
const [isPopState, setIsPopState] = useState(false);
const [isPending, startTransition] = useTransition();
const [version, setVersion] = useState(0);
const [navError, setNavError] = useState(null);
// navigate(), context bindings, scroll restorers...
// RSC payload resolve routing content:
const rscKey = route + "::" + version;
const content = navError ? getErrorRSCPayload(route, navError) : getRSCPayload(rscKey);
return (
<RouterContext.Provider value={contextValue}>
<ErrorBoundary key={navError ? "error" : "normal"} onError={setNavError}>
{use(content)}
</ErrorBoundary>
</RouterContext.Provider>
);
}
hydrateRoot(document, <Router />);📦 Webpack Variant (client-error-webpack.jsx)
Like standard hydration, when the Webpack build tool is active, the bundler maps input targets to client-error-webpack.jsx:
// In client-error-webpack.jsx
import { createFromFetch } from "react-server-dom-webpack/client";
import { createServerFunctionProxy } from "./server-function-proxy-webpack.js";It retains the exact same POST body payload overrides and isInitialErrorLoad gates while resolving Client Component symbols using Webpack's module registers.