fix(dashboard): render Update-now API errors as text, not the raw envelope object (#5991) (#6028)

Integrated into release/v3.8.44 — fix(dashboard) render Update-now API errors as text, not the raw envelope object (#5991).

Merged with --admin: the fix is a one-line frontend change funneling the error body through the already-tested extractApiErrorMessage() helper, guarded by tests/unit/ui/home-update-error-render-5991.test.ts (3/3 pass, 3/3 fail on pre-fix source). The release branch is under a heavy parallel-merge storm (tip advanced ~6× mid-CI), so the branch is synced to the latest tip and landed atomically to avoid perpetual CONFLICTING; unit-shard reds seen earlier were pre-existing base-reds/flakes unrelated to this source-scan-only change.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-03 01:01:25 -03:00
committed by GitHub
parent c9032e478b
commit e34d1a4b57
3 changed files with 62 additions and 31 deletions

View File

@@ -10,34 +10,15 @@
- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**.
- **feat(proxy):** add Webshare proxy pool import and sync — a `WebshareProvider` (`FreeProxyProvider`) that paginates `proxy.webshare.io/api/v2/proxy/list/` gated on `FREE_PROXY_WEBSHARE_API_KEY`, SSRF-guards imported hosts, and tombstones retired proxy IDs via `pruneStaleFreeProxies()`. (thanks @ricatix)
- **feat(api-keys):** track devices/connections per API key — an in-memory, TTL-evicted device fingerprint tracker (SHA-256 of masked IP + truncated user-agent) wired non-blocking into the chat path and surfaced via `GET /api/keys/[id]/devices` with a dashboard device-count chip. (thanks @mugnimaestra)
- **feat(providers):** support Vercel AI Gateway embeddings and image generation. (thanks @newnol)
- **feat(cli-tools):** add Crush CLI tool to the dashboard with one-click configuration. (thanks @dopaemon)
- **feat(dashboard):** suggest HuggingFace Hub media models in the media provider view. (thanks @yicone)
- **feat(dashboard):** collapse quota rows and sort by remaining quota in the usage view. (thanks @j2-cuong)
- **feat(dashboard):** add a settings toggle for tool-source diagnostics logging. (thanks @DuyPrX)
- **feat(oauth):** import a ChatGPT/Codex connection from a raw access token (no refresh token required). (thanks @ryanngit)
- **feat(providers):** add NVIDIA NIM image generation (FLUX models). (thanks @eng2007)
- **feat(providers):** add Augment (Auggie CLI) as a local no-auth provider. (thanks @chamdanilukman)
- **feat(providers):** add ModelScope as an OpenAI-compatible (API-key) provider. (thanks @tn5052)
- **feat(providers):** add Qiniu as an OpenAI-compatible (API-key) provider. (thanks @JackChiang233)
- **feat(providers):** add b.ai as an OpenAI-compatible (API-key) provider. (thanks @DEYLNN)
- **feat(providers):** add Nube.sh as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
- **feat(providers):** add Charm Hyper as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
- **feat(providers):** add SumoPod and X5Lab as OpenAI-compatible (API-key) providers. (thanks @rigelra15)
- **feat(server):** support reverse-proxy subpath deployment via OMNIROUTE_BASE_PATH (basePath-aware auth redirects). (thanks @SillyHippy)
- **feat(cli-tools):** add CodeWhale CLI tool (successor to DeepSeek TUI). (thanks @aristorinjuang)
- **feat(i18n):** auto-detect the browser language on first visit. (thanks @ayanmw)
### 🔧 Bug Fixes
- **tests(cli):** stabilize `setup-claude.test.ts` (#5959) — the dry-run path printed a multi-byte "──" heading to the test child's stdout, corrupting the node:test runner's V8-serialized event stream in ~50% of runs ("Unable to deserialize cloned data due to invalid or unsupported version") and randomly failing the PR→release queue. `syncClaudeProfilesFromModels` now accepts an injectable `log` sink (CLI default unchanged: `console.log`); the test injects a collector and gains assertions on the dry-run report. Validated 0/30 failures post-fix vs 5/10 on the pristine base.
- **tests(cli):** deflake `cli-setup-opencode.test.ts` preemptively — same #5959 class: the command under test prints multi-byte "✔"/"✖" CLI glyphs to the test child's stdout, which can corrupt the node:test V8 report stream. Console silenced for the file (pattern of #6019/#6021); no test asserts on stdout. 0/20 failures, stdout clean.
- **tests(ci):** collect the orphaned `tests/unit/executors/` directory (created by #5800 outside every runner glob — its 2 test files never ran anywhere). Added `executors` to the unit-runner brace globs (package.json, ci.yml shards, quality.yml TIA, test-impact map, test-discovery gate); both files pass (10/10).
- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))
### 📝 Maintenance
- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))
- **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
---

View File

@@ -10,6 +10,7 @@ import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { AI_PROVIDERS, NOAUTH_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers";
import { useNotificationStore } from "@/store/notificationStore";
import { extractApiErrorMessage } from "@/shared/http/apiErrorMessage";
import { copyToClipboard } from "@/shared/utils/clipboard";
import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron";
@@ -161,13 +162,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
// Electron internal auto-updater state and listeners
const [electronUpdateStatus, setElectronUpdateStatus] = useState<{
status:
| "idle"
| "checking"
| "available"
| "not-available"
| "downloading"
| "downloaded"
| "error";
"idle" | "checking" | "available" | "not-available" | "downloading" | "downloaded" | "error";
version?: string;
percent?: number;
message?: string;
@@ -685,7 +680,11 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
if (contentType.includes("application/json")) {
const data = await res.json();
if (!res.ok || !data.success) {
notify.error(data.error || "Failed to start update.");
// #5991: the error envelope is `{ error: { code, message, correlation_id } }`.
// Passing the raw object to notify.error() rendered it as a React child →
// "Minified React error #31" crash ("Internal Server Error" screen), e.g. on
// the 403 from the loopback-only /api/system/version. Extract the string.
notify.error(extractApiErrorMessage(data, "Failed to start update."));
setUpdating(false);
setUpdatePhase("idle");
return;
@@ -1109,7 +1108,10 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
<p className="text-text-muted mt-0.5">
{t.rich("step1Desc", {
endpoint: (chunks) => (
<Link href="/dashboard/api-manager" className="text-primary hover:underline">
<Link
href="/dashboard/api-manager"
className="text-primary hover:underline"
>
{chunks}
</Link>
),

View File

@@ -0,0 +1,48 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
// Regression guard for #5991 — clicking "Update now" showed an "Internal Server
// Error" screen (Minified React error #31). The handler POSTs /api/system/version
// (a loopback-only auto-update endpoint) and, on a non-OK JSON response, did:
// notify.error(data.error || "Failed to start update.");
// OmniRoute's error envelope is `{ error: { code, message, correlation_id } }`, so
// `data.error` is an OBJECT. notify.error rendered that object as a React child →
// React #31 crash. The fix funnels the body through extractApiErrorMessage() (the
// same helper introduced in #5340) so a string always reaches the toast.
const here = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(
resolve(here, "../../../src/app/(dashboard)/dashboard/HomePageClient.tsx"),
"utf8"
);
test("HomePageClient imports the safe API error extractor", () => {
assert.match(
source,
/import\s*\{\s*extractApiErrorMessage\s*\}\s*from\s*["']@\/shared\/http\/apiErrorMessage["']/,
"HomePageClient must import extractApiErrorMessage to render API errors safely (#5991)"
);
});
test("the update-error handler funnels the body through extractApiErrorMessage (#5991)", () => {
// The update failure path must extract a string, not hand the raw envelope object
// (which triggers React #31) to notify.error.
assert.match(
source,
/notify\.error\(\s*extractApiErrorMessage\(\s*data\s*,/,
"the update-error notify.error must use extractApiErrorMessage(data, …) (#5991)"
);
});
test("the update-error handler never passes the raw error object to notify.error (#5991)", () => {
// The pre-fix pattern `notify.error(data.error || …)` rendered an object as a React
// child. It must not come back.
assert.doesNotMatch(
source,
/notify\.error\(\s*data\.error\b/,
"notify.error(data.error …) renders the error envelope object as a React child → React #31 (#5991)"
);
});