crypto.randomUUID Fails on LAN HTTP: Use a getRandomValues Fallback
crypto.randomUUID() works in local development, then your app crashes when opened from a phone over http://192.168.x.x.
That is not random. crypto.randomUUID() is restricted to secure contexts. https:// is secure. http://localhost is treated as secure for development. A LAN IP over plain HTTP is not.
If your client-side app may be opened from another device on the same network, do not call crypto.randomUUID() without a fallback.
The misleading local test
This works:
http://localhost:5173
This may fail:
http://192.168.1.20:5173
The code is the same:
const id = crypto.randomUUID();
The origin changed. Browser security rules changed with it.
Use getRandomValues for fallback UUID generation
crypto.getRandomValues() is available more broadly and can be used to generate a UUID v4-style value:
export function createBrowserId(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
try {
return crypto.randomUUID();
} catch {
// Fall through for non-secure contexts such as LAN HTTP.
}
}
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20),
].join("-");
}
Use this helper everywhere the browser generates IDs.
When this matters
Add the fallback when:
- a dev server is tested from a phone or tablet
- the app displays a QR code for LAN access
- a self-hosted app runs over home-network HTTP
- IDs are generated in React components or browser stores
- the app works on localhost but fails on another device
You do not need this for server-side Node.js code using Node's crypto API, or for browser apps that are always served over HTTPS.
Do not silently downgrade security-sensitive IDs
This fallback is for ordinary client-generated identifiers: temporary UI IDs, local records, optimistic rows, tab IDs, and drafts.
For security-sensitive identifiers, tokens, authentication, password reset links, license keys, or payment references, generate them server-side or in a trusted backend. Do not rely on client-side UUID generation as a security boundary.
Add a regression test around the helper
You can test the fallback path by making randomUUID throw:
import { expect, test, vi } from "vitest";
import { createBrowserId } from "./createBrowserId";
test("falls back when randomUUID throws", () => {
const originalCrypto = globalThis.crypto;
vi.stubGlobal("crypto", {
randomUUID: () => {
throw new Error("secure context required");
},
getRandomValues: (array: Uint8Array) => {
array.fill(1);
return array;
},
});
expect(createBrowserId()).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);
vi.stubGlobal("crypto", originalCrypto);
});
The exact random value is not important. The UUID shape and version bits are.
Debugging checklist
If an app fails only on a phone or LAN URL:
- Search for
crypto.randomUUID(). - Check whether the failing origin is plain HTTP and not localhost.
- Replace direct calls with a helper that catches and falls back.
- Keep security-sensitive IDs on the server.
- Retest on the LAN URL, not only on localhost.
References
Summary
crypto.randomUUID() is convenient but origin-sensitive. It can pass every localhost test and fail on LAN HTTP. Wrap it once, catch secure-context failures, and use getRandomValues() as a browser fallback for non-sensitive client IDs.