From 4d3fc04278abda01daeb58f614829fa881e7c5bc Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 3 Sep 2026 18:22:09 -0700 Subject: [PATCH] Fix uploads failing when Kanna is opened over plain http MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `crypto.randomUUID()` is a secure-context-only API: it exists on https origins and on localhost/127.0.0.1, and nowhere else. Opening Kanna over a LAN or Tailscale hostname (http://host.ts.net:5174) leaves it undefined, so the first line of per-file upload work threw a TypeError before the attachment placeholder was added and before the request was sent. The throw is synchronous and sits outside the try/catch that fills in `uploadError`, so nothing appeared in the composer and no error was shown — the upload just silently did nothing. It also leaked `activeUploadsRef`, incremented on the line above and only decremented in the `finally` of the async block below it, so after three attempts the queue wedged until reload. Use `generateUUID()` from lib/utils, which already falls back to a Math.random UUID when `crypto.randomUUID` is missing. The ids are composer-local temporaries and router request tokens, so a non-cryptographic fallback is fine for both. `buildChatJumpLocationState` had the same bug, which broke jump-to-message from the sidebar and minimap on the same page. 🌸 Shipped with Kanna — https://kanna.sh Co-Authored-By: Kanna Kanna-Agent: claude/opus[1m] --- src/client/components/chat-ui/ChatInput.tsx | 7 +++++-- src/client/lib/chat-navigation.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/client/components/chat-ui/ChatInput.tsx b/src/client/components/chat-ui/ChatInput.tsx index dcd732008..cdb1a79ff 100644 --- a/src/client/components/chat-ui/ChatInput.tsx +++ b/src/client/components/chat-ui/ChatInput.tsx @@ -14,7 +14,7 @@ import { import { Button } from "../ui/button" import { Textarea } from "../ui/textarea" import { ScrollArea } from "../ui/scroll-area" -import { cn } from "../../lib/utils" +import { cn, generateUUID } from "../../lib/utils" import { useComposer } from "../../hooks/useComposer" import { useIsStandalone } from "../../hooks/useIsStandalone" import { useVoiceRecorder } from "../../hooks/useVoiceRecorder" @@ -540,7 +540,10 @@ const ChatInputInner = forwardRef(function ChatInput({ if (!file) break activeUploadsRef.current += 1 - const tempId = crypto.randomUUID() + // Not crypto.randomUUID(): that is secure-context only, so it is + // undefined when Kanna is opened over plain http on a LAN/Tailscale + // hostname, and every upload would throw before it started. + const tempId = generateUUID() const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined const generation = uploadGenerationRef.current diff --git a/src/client/lib/chat-navigation.ts b/src/client/lib/chat-navigation.ts index f4b9ce51d..1e8928ba0 100644 --- a/src/client/lib/chat-navigation.ts +++ b/src/client/lib/chat-navigation.ts @@ -1,3 +1,5 @@ +import { generateUUID } from "./utils" + /** * The router-state contract for opening a chat *at a message*. * @@ -29,7 +31,9 @@ export interface ChatJumpLocationState { } export function buildChatJumpLocationState(role: ChatJumpRole): ChatJumpLocationState { - return { jumpToRole: role, jumpRequestId: crypto.randomUUID() } + // generateUUID, not crypto.randomUUID: the latter is secure-context only and + // is undefined over plain http on a LAN/Tailscale hostname. + return { jumpToRole: role, jumpRequestId: generateUUID() } } /** Reads the jump out of an opaque `useLocation().state`, or null if absent. */