crypto.randomUUIDがLAN HTTPで失敗する時はgetRandomValues fallbackを使う
crypto.randomUUID() はlocal developmentでは動くのに、phoneから http://192.168.x.x で開くと落ちることがあります。
これは偶然ではありません。crypto.randomUUID() はsecure contextに制限されています。https:// はsecureです。http://localhost も開発用にsecure扱いされます。しかしplain HTTPのLAN IPはsecureではありません。
同じnetwork上の別deviceからclient-side appを開く可能性があるなら、crypto.randomUUID() を直接呼ばずfallbackを用意します。
誤解しやすいlocal test
これは動きます。
http://localhost:5173
これは失敗することがあります。
http://192.168.1.20:5173
codeは同じです。
const id = crypto.randomUUID();
originが変わったことで、browser security ruleも変わります。
fallback UUID生成にgetRandomValuesを使う
crypto.getRandomValues() はより広いcontextで使え、UUID v4風の値を生成できます。
export function createBrowserId(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
try {
return crypto.randomUUID();
} catch {
// LAN HTTPなどのnon-secure contextではfallbackする
}
}
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("-");
}
browserでIDを生成する箇所は、このhelperに寄せます。
使うべき場面
fallbackを入れる場面です。
- dev serverをphone/tabletから開く
- LAN access用のQR codeを表示する
- self-hosted appをhome-network HTTPで使う
- React componentやbrowser storeでIDを作る
- localhostでは動くが別deviceで失敗する
Node.js server-side codeや、常にHTTPSで配信されるbrowser appでは不要なこともあります。
security-sensitive IDには使わない
このfallbackは、temporary UI ID、local record、optimistic row、tab ID、draft IDのような通常のclient-generated identifier向けです。
token、authentication、password reset、license key、payment referenceのようなsecurity-sensitive IDは、server-sideまたはtrusted backendで生成します。client-side UUIDをsecurity boundaryにしてはいけません。
fallback pathのtestを入れる
randomUUID がthrowする形でtestできます。
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);
});
重要なのはrandom値そのものではなく、UUID shapeとversion bitです。
デバッグチェック
phoneやLAN URLだけで落ちる場合:
crypto.randomUUID()を検索する- 失敗originがplain HTTPかつlocalhostではないか確認する
- direct callをhelperへ置き換える
- security-sensitive IDはserver側に置く
- localhostだけでなくLAN URLで再テストする