Skip to content

Commit 977f970

Browse files
committed
fix(rivetkit): surface native runtime load failures instead of masking them with wasm
1 parent e8ddfc9 commit 977f970

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

rivetkit-typescript/packages/rivetkit/src/registry/native.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,36 @@ export async function loadAutoRuntime(
224224
return (await loaders.loadWasm(config.wasm)).runtime;
225225
}
226226

227+
let nativeError: unknown;
227228
try {
228229
return (await loaders.loadNative()).runtime;
229-
} catch {
230+
} catch (error) {
231+
nativeError = error;
232+
// Native is the expected runtime on a node-like host, so this is the
233+
// actionable error even when the wasm fallback goes on to succeed.
234+
// Discarding it hides causes such as a platform binding that npm
235+
// silently skipped, which then resurfaces as an unrelated wasm error.
236+
logger().warn({
237+
msg: "native runtime failed to load; falling back to wasm",
238+
error: stringifyError(error),
239+
});
240+
}
241+
242+
try {
230243
return (await loaders.loadWasm(config.wasm)).runtime;
244+
} catch (wasmError) {
245+
// Report both, native first. The wasm failure on a node-like host is
246+
// usually just its loader fetching over `file://`, which says nothing
247+
// about why native was unavailable.
248+
throw new RivetError(
249+
"config",
250+
"runtime_unavailable",
251+
`RivetKit could not load a core runtime. Native runtime: ${stringifyError(nativeError)} Wasm runtime: ${stringifyError(wasmError)}`,
252+
{
253+
public: true,
254+
statusCode: 500,
255+
},
256+
);
231257
}
232258
}
233259

rivetkit-typescript/packages/rivetkit/src/registry/runtime.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { describe, expect, test } from "vitest";
2+
import type { RegistryConfig } from "./config";
3+
import { loadAutoRuntime, type RuntimeLoaders } from "./native";
24
import {
5+
type CoreRuntime,
36
normalizeRuntimeSqlExecuteResult,
47
type RuntimeSqlBindParam,
58
type RuntimeSqlBindParams,
@@ -48,3 +51,74 @@ describe("runtime SQL boundary", () => {
4851
expect(normalizeRuntimeSqlExecuteResult(base)).toEqual(base);
4952
});
5053
});
54+
55+
describe("loadAutoRuntime failure reporting", () => {
56+
const wasmRuntime = { kind: "wasm" } as unknown as CoreRuntime;
57+
const nativeRuntime = { kind: "napi" } as unknown as CoreRuntime;
58+
const config = {} as RegistryConfig;
59+
60+
function loaders(overrides: Partial<RuntimeLoaders>): RuntimeLoaders {
61+
return {
62+
detectHost: () => "node-like",
63+
loadNative: async () => ({ runtime: nativeRuntime }) as never,
64+
loadWasm: async () => ({ runtime: wasmRuntime }) as never,
65+
...overrides,
66+
};
67+
}
68+
69+
test("prefers native when it loads", async () => {
70+
const runtime = await loadAutoRuntime(config, loaders({}));
71+
expect(runtime).toBe(nativeRuntime);
72+
});
73+
74+
test("falls back to wasm when native fails", async () => {
75+
const runtime = await loadAutoRuntime(
76+
config,
77+
loaders({
78+
loadNative: async () => {
79+
throw new Error("missing platform binding");
80+
},
81+
}),
82+
);
83+
expect(runtime).toBe(wasmRuntime);
84+
});
85+
86+
test("reports the native cause when both runtimes fail", async () => {
87+
// The native failure is the actionable one. Before this was reported,
88+
// a skipped platform binding surfaced only as the wasm loader's
89+
// unrelated `file://` fetch error.
90+
const promise = loadAutoRuntime(
91+
config,
92+
loaders({
93+
loadNative: async () => {
94+
throw new Error(
95+
"Cannot find module '@rivetkit/rivetkit-napi-linux-x64-musl'",
96+
);
97+
},
98+
loadWasm: async () => {
99+
throw new Error("fetch failed");
100+
},
101+
}),
102+
);
103+
await expect(promise).rejects.toThrow(/rivetkit-napi-linux-x64-musl/);
104+
await expect(promise).rejects.toThrow(/fetch failed/);
105+
});
106+
107+
test("uses wasm directly on an edge-like host without touching native", async () => {
108+
let nativeCalls = 0;
109+
const runtime = await loadAutoRuntime(
110+
config,
111+
loaders({
112+
detectHost: () => "edge-like",
113+
loadNative: async () => {
114+
nativeCalls += 1;
115+
throw new Error(
116+
"native must not be attempted on edge hosts",
117+
);
118+
},
119+
}),
120+
);
121+
expect(runtime).toBe(wasmRuntime);
122+
expect(nativeCalls).toBe(0);
123+
});
124+
});

0 commit comments

Comments
 (0)