← ./articles

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:

  1. Search for crypto.randomUUID().
  2. Check whether the failing origin is plain HTTP and not localhost.
  3. Replace direct calls with a helper that catches and falls back.
  4. Keep security-sensitive IDs on the server.
  5. 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.