Skip to content

fix(docker): runtime API-base override (Settings 'Failed to load engines: Failed to fetch') - #174

Merged
debpalash merged 1 commit into
mainfrom
fix/docker-api-base
May 30, 2026
Merged

fix(docker): runtime API-base override (Settings 'Failed to load engines: Failed to fetch')#174
debpalash merged 1 commit into
mainfrom
fix/docker-api-base

Conversation

@debpalash

@debpalash debpalash commented May 30, 2026

Copy link
Copy Markdown
Owner

Bug

Docker users open the web UI, go to Settings → Engines, and get "Failed to load engines: Failed to fetch" (a browser TypeError — the request never reached a server). Works in the desktop build.

Root cause

  1. Two divergent API-base resolvers: client.ts (what the Engines tab uses) honored VITE_API_URL; utils/apiBase.ts (media) honored VITE_OMNIVOICE_API. The docs told users to set VITE_OMNIVOICE_APIwhich the Engines path ignored.
  2. No working runtime override: VITE_* is inlined at build time, so the prebuilt ghcr.io image can't take an override from docker run -e — leaving reverse-proxy / split-origin deployments with no knob at all. (The standard single-container localhost:3900 case already works via same-origin; the failure is non-same-origin topologies.)

Fix

  • Backend (core/spa_inject.py + main.py): when OMNIVOICE_PUBLIC_API_BASE is set, inject it into index.html as window.__OMNIVOICE_API_BASE__. The value is validated to a plain http(s):// URL (no quotes/angle brackets) so it can't break out of the <script>. Unset (default) → StaticFiles serves index.html untouched — same-origin, zero overhead, no behavior change.
  • Frontend (client.ts + utils/apiBase.ts): both resolvers now read the runtime global first, then VITE_OMNIVOICE_API / VITE_API_URL, then fall through to same-origin. client.ts also strips trailing slashes and recognizes __TAURI_INTERNALS__ (parity with the other resolvers).
  • Docs: docker.md + troubleshooting.md now document OMNIVOICE_PUBLIC_API_BASE — the runtime override that actually works on the prebuilt image.

Verify

  • Backend pytest tests/test_spa_inject.py (5) ✓, router smoke (23) ✓.
  • Frontend typecheck ✓, resolver tests 16/16 (runtime-global wins; VITE_OMNIVOICE_API honored; trailing-slash strip; __TAURI_INTERNALS__), full vitest 107/107, build ✓.
  • In Docker behind a proxy: docker run -e OMNIVOICE_PUBLIC_API_BASE=https://api.host … → Engines tab loads (request targets the configured origin). Standard localhost:3900 unchanged.

Default same-origin behavior identical on macOS/Windows/Linux; override is opt-in.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added runtime API base configuration via OMNIVOICE_PUBLIC_API_BASE environment variable, allowing dynamic API endpoint setup for Docker deployments without rebuilding the application.
  • Documentation

    • Updated Docker and troubleshooting guides to explain runtime API base configuration for multi-origin setups, including examples and expected URL format.

Review Change Stack

…e backend

Docker users reported Settings -> Engines failing with 'Failed to load engines:
Failed to fetch'. Two problems: (1) the two API-base resolvers diverged
(client.ts honored VITE_API_URL; apiBase.ts honored VITE_OMNIVOICE_API), and
the docs documented VITE_OMNIVOICE_API -- which the Engines request path
ignored; (2) VITE_* is inlined at BUILD time, so a prebuilt ghcr.io image has
no working runtime override at all for reverse-proxy / split-origin deploys.

- backend: when OMNIVOICE_PUBLIC_API_BASE is set, inject it into index.html as
  window.__OMNIVOICE_API_BASE__ (core/spa_inject.py; validated to a plain
  http(s) URL so it can't break out of the <script>). Unset (default) ->
  StaticFiles serves index.html untouched (same-origin, zero overhead).
- frontend: both resolvers (client.ts _resolveApiBase + utils/apiBase.ts) now
  read the runtime global FIRST, then VITE_OMNIVOICE_API/VITE_API_URL, then
  fall through to same-origin. client.ts also strips trailing slashes and
  recognises __TAURI_INTERNALS__ (parity with apiBase.ts/external.ts).
- docs: docker.md + troubleshooting.md document OMNIVOICE_PUBLIC_API_BASE as the
  runtime override that works on the prebuilt image (the old VITE_OMNIVOICE_API
  docker run -e example never worked -- build-time inlining).
- tests: spa_inject helpers (inject + URL validation/breakout); resolver
  precedence for the runtime global + VITE_OMNIVOICE_API in both test files.

Default same-origin behavior is unchanged on every platform; override is opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a runtime API base override mechanism for SPA deployments. A new backend module validates and injects a window.__OMNIVOICE_API_BASE__ global into the served index.html. The frontend's API resolution logic checks this injected value first, before build-time environment variables or Tauri loopback fallbacks. Documentation and tests support both the injection and resolution behavior.

Changes

Runtime API Base Override

Layer / File(s) Summary
Backend SPA injection helper module and validation tests
backend/core/spa_inject.py, tests/test_spa_inject.py
New spa_inject.py module provides regex-validated URL sanitation and injects a <script> tag setting window.__OMNIVOICE_API_BASE__ into the HTML after <head> or at the start if no head exists. Tests verify URL rejection of malicious payloads and proper HTML injection placement and JSON encoding.
Backend route integration for injected HTML serving
backend/main.py
Conditionally reads OMNIVOICE_PUBLIC_API_BASE, validates it, injects the value into the built frontend HTML on startup, and registers GET handlers for / and /index.html to serve the modified HTML when the environment variable is valid and the file exists.
Frontend type augmentation and API base resolution logic
frontend/src/utils/apiBase.ts, frontend/src/api/client.ts
Adds window.__OMNIVOICE_API_BASE__ to the Window type, then updates both getApiBase() and _resolveApiBase() to prioritize the injected value (with trailing-slash stripping) before VITE_OMNIVOICE_API, VITE_API_URL, and Tauri/browser fallbacks. Expands Tauri detection to include __TAURI_INTERNALS__.
Frontend test coverage for runtime override resolution
frontend/src/utils/apiBase.test.ts, frontend/src/api/client.apibase.test.ts
Tests verify highest precedence for window.__OMNIVOICE_API_BASE__ in both resolution functions, that VITE_OMNIVOICE_API is honored as a fallback, trailing-slash normalization, and correct Tauri loopback detection. Test cleanup removes the injected global between cases.
Docker setup and troubleshooting documentation updates
docs/install/docker.md, docs/install/troubleshooting.md
Updated Docker docs to explain same-origin defaults and introduce OMNIVOICE_PUBLIC_API_BASE as the runtime override for cross-origin scenarios (replacing build-time VITE_OMNIVOICE_API). Added docker run -e examples and updated troubleshooting to guide users to set the runtime variable for reverse-proxy deployments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • debpalash/OmniVoice-Studio#123: Both PRs modify frontend API base resolution in frontend/src/api/client.ts—this PR adds runtime window.__OMNIVOICE_API_BASE__ precedence while that PR changes the default host derivation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: a runtime API-base override for Docker deployments that resolves the 'Failed to fetch' error in Settings.
Description check ✅ Passed The PR description comprehensively covers bug, root cause, fix, and verification. All template sections (Summary, Changes, Type, Testing, Checklist) are complete with appropriate details and checkmarks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/docker-api-base

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the "Failed to load engines: Failed to fetch" error for Docker users by introducing a runtime API-base override mechanism (OMNIVOICE_PUBLIC_API_BASE) that works with prebuilt images — where VITE_* build-time vars cannot be changed. It also unifies two previously divergent API-base resolvers (client.ts and utils/apiBase.ts) so both honor the same precedence chain.

  • Backend (core/spa_inject.py + main.py): when OMNIVOICE_PUBLIC_API_BASE is set, validates it and injects window.__OMNIVOICE_API_BASE__ into index.html via dedicated GET / and GET /index.html routes before the StaticFiles mount. Unset → zero behavior change.
  • Frontend (client.ts + utils/apiBase.ts): both resolvers now check the runtime global first, then VITE_OMNIVOICE_API, then VITE_API_URL, then same-origin; client.ts gains __TAURI_INTERNALS__ parity and trailing-slash stripping.
  • Docs + tests: docker.md and troubleshooting.md updated to the new variable; 5 Python + 16 TypeScript resolver tests added.

Confidence Score: 3/5

Safe to merge for the common same-origin case; the Docker reverse-proxy override path has a gap where page refreshes on any route other than / or /index.html receive the un-injected HTML, silently falling back to same-origin and breaking the feature the PR set out to fix.

The core injection mechanism is well-implemented and safe. The gap is in main.py: only GET / and GET /index.html serve the modified HTML — every other SPA route (e.g. a user refreshing at /settings) falls through to the raw StaticFiles mount, so window.__OMNIVOICE_API_BASE__ is never set and the API calls silently fall back to same-origin. This is a present defect in the changed code path, not a theoretical one, and it would cause the 'Failed to fetch' regression to reappear for any Docker user who refreshes or bookmarks a non-root URL.

backend/main.py — the injected route registration needs to cover all non-asset SPA paths, not just / and /index.html.

Important Files Changed

Filename Overview
backend/core/spa_inject.py New pure-function module: URL validation via regex and HTML <head> injection of the runtime global; JSON-encodes the value to prevent script breakout. Well-tested.
backend/main.py Registers GET / and GET /index.html to serve the injected index.html, but SPA deep-link navigations to any other path (e.g. /settings) still fall through to StaticFiles and receive the un-injected file. Also reads from disk on every request instead of caching at startup.
frontend/src/api/client.ts Unified API-base resolver: runtime global → VITE_OMNIVOICE_API → VITE_API_URL → same-origin, with trailing-slash stripping and __TAURI_INTERNALS__ parity. Clean.
frontend/src/utils/apiBase.ts Added runtime global check (priority 0) ahead of the existing env/Tauri/origin chain; consistent with client.ts precedence order.
tests/test_spa_inject.py Good unit coverage: valid/invalid URL cases, <head> injection, no-head fallback, and JSON encoding. All pure-function tests with no app bootstrapping required.
frontend/src/api/client.apibase.test.ts New test cases cover runtime global precedence (beats Tauri + VITE_API_URL), VITE_OMNIVOICE_API alias, and TAURI_INTERNALS loopback.
frontend/src/utils/apiBase.test.ts Adds runtime global test; afterEach properly cleans up __OMNIVOICE_API_BASE__ to prevent test pollution.
docs/install/docker.md Docs updated to replace VITE_OMNIVOICE_API with OMNIVOICE_PUBLIC_API_BASE; clearly explains runtime vs build-time distinction.
docs/install/troubleshooting.md Updated troubleshooting guide to reference the new runtime override variable.

Sequence Diagram

sequenceDiagram
    participant Op as Operator
    participant D as Docker / Env
    participant B as FastAPI Backend
    participant S as StaticFiles Mount
    participant Br as Browser

    Op->>D: "OMNIVOICE_PUBLIC_API_BASE=https://api.host"
    D->>B: startup env var read

    B->>B: is_valid_public_api_base() → true
    B->>B: register GET / and GET /index.html routes

    Br->>B: GET /
    B->>B: read index.html + inject_api_base()
    B-->>Br: index.html with window.__OMNIVOICE_API_BASE__

    Note over Br: module-level API constant resolved to https://api.host ✓

    Br->>B: GET /settings (page refresh / deep link)
    B->>S: no matching route → StaticFiles handles
    S-->>Br: raw index.html (no injection ⚠)

    Note over Br: window.__OMNIVOICE_API_BASE__ undefined, API falls back to same-origin ✗
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(docker): runtime API-base override s..." | Re-trigger Greptile

Comment thread backend/main.py
Comment on lines +648 to 656
@app.get("/", include_in_schema=False)
def _index_root():
return _index_with_api_base()

@app.get("/index.html", include_in_schema=False)
def _index_html():
return _index_with_api_base()

app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 SPA deep-link navigation bypasses injection

Only GET / and GET /index.html are intercepted by the injected routes; every other path (e.g. a user refreshing at /settings or /engines, or opening a bookmarked URL) falls through to the StaticFiles mount, which serves the raw index.html without window.__OMNIVOICE_API_BASE__. Because client.ts evaluates API as a module-level constant at import time, those page loads will resolve the API base as same-origin (or fall back to loopback), silently ignoring OMNIVOICE_PUBLIC_API_BASE. The fix is to add a catch-all route (or a middleware) before the StaticFiles mount that serves the injected HTML for all non-asset paths — the same pattern used by most single-page-app servers.

Fix in Claude Code

Comment thread backend/main.py
Comment on lines +641 to +646
if _public_api_base and os.path.isfile(_index_path):
from fastapi.responses import HTMLResponse

def _index_with_api_base() -> "HTMLResponse":
with open(_index_path, "r", encoding="utf-8") as _fh:
return HTMLResponse(inject_api_base(_fh.read(), _public_api_base))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 File read on every request — cache the injected HTML at startup

_public_api_base is resolved once at startup (it never changes at runtime), so the injected HTML will always be identical. Reading and injecting on every / and /index.html request wastes disk I/O unnecessarily. Cache the result at startup instead.

Suggested change
if _public_api_base and os.path.isfile(_index_path):
from fastapi.responses import HTMLResponse
def _index_with_api_base() -> "HTMLResponse":
with open(_index_path, "r", encoding="utf-8") as _fh:
return HTMLResponse(inject_api_base(_fh.read(), _public_api_base))
if _public_api_base and os.path.isfile(_index_path):
from fastapi.responses import HTMLResponse
with open(_index_path, "r", encoding="utf-8") as _fh:
_injected_html = inject_api_base(_fh.read(), _public_api_base)
def _index_with_api_base() -> "HTMLResponse":
return HTMLResponse(_injected_html)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
docs/install/docker.md (2)

82-86: 💤 Low value

Remove blank line between blockquote paragraphs.

Markdown linting flags the blank line at line 86 inside the blockquote block. Removing it improves formatting consistency.

📝 Suggested change
 > `OMNIVOICE_PUBLIC_API_BASE` must be a plain `http(s)://…` URL; anything else
 > is ignored and the app falls back to same-origin. If you build from source you
 > may instead bake `VITE_OMNIVOICE_API` at build time, but the runtime var above
 > is simpler and image-agnostic.
-
 > **Security:** OmniVoice ships no authentication. Anything on your LAN with
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install/docker.md` around lines 82 - 86, Remove the extra blank line
inside the blockquote in docs/install/docker.md between the two sentences
mentioning `OMNIVOICE_PUBLIC_API_BASE` and `VITE_OMNIVOICE_API` so the two lines
form a single continuous blockquote paragraph (i.e., delete the empty line after
the sentence referencing `OMNIVOICE_PUBLIC_API_BASE`), keeping the same wording
and markdown blockquote markers.

68-68: 💤 Low value

Consider American English variant for consistency.

The word "afterwards" is more common in British English. For American English documentation, "afterward" is preferred.

📝 Suggested change
-from, so opening the UI from `http://<lan-ip>:3900` Just Works for both the
-page load *and* the API/media requests it makes afterwards.
+from, so opening the UI from `http://<lan-ip>:3900` Just Works for both the
+page load *and* the API/media requests it makes afterward.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/install/docker.md` at line 68, Replace the British English adverb
"afterwards" in the sentence containing "page load *and* the API/media requests
it makes afterwards." with the American English variant "afterward" so the
sentence reads "...the API/media requests it makes afterward." to maintain
consistency in the docs; update the single occurrence of the string "afterwards"
accordingly.
backend/main.py (1)

625-656: Runtime override looks correct; note the CORS prerequisite for split-origin.

The explicit / and /index.html handlers are registered before the StaticFiles mount at /, so Starlette matches them first — injection wins for the shell while assets still fall through to the mount. Logic is sound.

One deployment caveat worth surfacing in the docs/log: when OMNIVOICE_PUBLIC_API_BASE points at a different origin than where the SPA is served (the reverse-proxy / split-origin case this PR targets), the browser will issue cross-origin requests to that base. Unless that SPA origin is added to OMNIVOICE_ALLOWED_ORIGINS (default is localhost only, Lines 554-557), the calls will be blocked by CORS and the user still sees "Failed to fetch" — just from a different cause. Worth calling out alongside the OMNIVOICE_PUBLIC_API_BASE instructions so operators set both together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 625 - 656, When OMNIVOICE_PUBLIC_API_BASE is
set to a different origin than the SPA host, cross-origin requests can be
blocked unless that SPA origin is present in OMNIVOICE_ALLOWED_ORIGINS; detect
this at startup and log a clear warning. After computing _public_api_base (and
validating via is_valid_public_api_base), parse its origin and check the
OMNIVOICE_ALLOWED_ORIGINS environment value (comma-separated) for that SPA
origin; if not present, emit a warning via
logging.getLogger("omnivoice.api").warning naming OMNIVOICE_PUBLIC_API_BASE and
advising operators to add the SPA origin to OMNIVOICE_ALLOWED_ORIGINS. Ensure
you reference the existing symbols _public_api_base, is_valid_public_api_base,
and OMNIVOICE_ALLOWED_ORIGINS so the change sits next to the current injection
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/main.py`:
- Around line 625-656: When OMNIVOICE_PUBLIC_API_BASE is set to a different
origin than the SPA host, cross-origin requests can be blocked unless that SPA
origin is present in OMNIVOICE_ALLOWED_ORIGINS; detect this at startup and log a
clear warning. After computing _public_api_base (and validating via
is_valid_public_api_base), parse its origin and check the
OMNIVOICE_ALLOWED_ORIGINS environment value (comma-separated) for that SPA
origin; if not present, emit a warning via
logging.getLogger("omnivoice.api").warning naming OMNIVOICE_PUBLIC_API_BASE and
advising operators to add the SPA origin to OMNIVOICE_ALLOWED_ORIGINS. Ensure
you reference the existing symbols _public_api_base, is_valid_public_api_base,
and OMNIVOICE_ALLOWED_ORIGINS so the change sits next to the current injection
logic.

In `@docs/install/docker.md`:
- Around line 82-86: Remove the extra blank line inside the blockquote in
docs/install/docker.md between the two sentences mentioning
`OMNIVOICE_PUBLIC_API_BASE` and `VITE_OMNIVOICE_API` so the two lines form a
single continuous blockquote paragraph (i.e., delete the empty line after the
sentence referencing `OMNIVOICE_PUBLIC_API_BASE`), keeping the same wording and
markdown blockquote markers.
- Line 68: Replace the British English adverb "afterwards" in the sentence
containing "page load *and* the API/media requests it makes afterwards." with
the American English variant "afterward" so the sentence reads "...the API/media
requests it makes afterward." to maintain consistency in the docs; update the
single occurrence of the string "afterwards" accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cfe258c6-065d-4b6f-9210-f36da2ec1ed2

📥 Commits

Reviewing files that changed from the base of the PR and between 40cf9bf and 934cdfc.

📒 Files selected for processing (9)
  • backend/core/spa_inject.py
  • backend/main.py
  • docs/install/docker.md
  • docs/install/troubleshooting.md
  • frontend/src/api/client.apibase.test.ts
  • frontend/src/api/client.ts
  • frontend/src/utils/apiBase.test.ts
  • frontend/src/utils/apiBase.ts
  • tests/test_spa_inject.py

@debpalash
debpalash merged commit 1b08c03 into main May 30, 2026
15 checks passed
@debpalash
debpalash deleted the fix/docker-api-base branch May 30, 2026 15:24
debpalash pushed a commit that referenced this pull request Jun 23, 2026
…ode-fallback log

Two coupled Windows fixes for the preview/blob audio path (the "playBlobAudio
decode error: EncodingError: Unable to decode audio data" users see in
Logs → Frontend on Windows).

1. apiBase 127.0.0.1, not localhost (Tauri context). The backend binds IPv4
   127.0.0.1 only; on Windows "localhost" often resolves to ::1 (IPv6) first, so
   requests miss the backend. The main client (api/client.ts) already did this
   since #174, but utils/apiBase.ts lagged on "localhost" — and its one consumer
   is utils/media.js's preview upload, the #653 fallback. So #653's streamed
   fallback fetched http://localhost:3900/preview/upload and FAILED on Windows,
   leaving preview playback broken even after #653. Align the two resolvers.

2. Quieter, accurate logging in playBlobAudio. The Web Audio decodeAudioData
   path is EXPECTED to fail for long-form / AAC renders on WebView2 and is
   recovered by the streamed fallback — yet it logged at error level, so users
   saw a red "decode error" even when playback succeeded. Downgrade that branch
   to console.warn ("falling back to streamed playback"); reserve error level for
   the real failure (both decode AND fallback failed). With fix #1 the fallback
   now actually reaches the backend on Windows, so the recovery completes.

Tests: apiBase.test.ts asserts Tauri → http://127.0.0.1:3900; the existing
playBlobAudioFallback.test.js (#653) still passes (fetch hits /preview/upload,
plays the HTTP URL, never a blob:).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
debpalash added a commit that referenced this pull request Jun 23, 2026
…ode-fallback log (#659)

Two coupled Windows fixes for the preview/blob audio path (the "playBlobAudio
decode error: EncodingError: Unable to decode audio data" users see in
Logs → Frontend on Windows).

1. apiBase 127.0.0.1, not localhost (Tauri context). The backend binds IPv4
   127.0.0.1 only; on Windows "localhost" often resolves to ::1 (IPv6) first, so
   requests miss the backend. The main client (api/client.ts) already did this
   since #174, but utils/apiBase.ts lagged on "localhost" — and its one consumer
   is utils/media.js's preview upload, the #653 fallback. So #653's streamed
   fallback fetched http://localhost:3900/preview/upload and FAILED on Windows,
   leaving preview playback broken even after #653. Align the two resolvers.

2. Quieter, accurate logging in playBlobAudio. The Web Audio decodeAudioData
   path is EXPECTED to fail for long-form / AAC renders on WebView2 and is
   recovered by the streamed fallback — yet it logged at error level, so users
   saw a red "decode error" even when playback succeeded. Downgrade that branch
   to console.warn ("falling back to streamed playback"); reserve error level for
   the real failure (both decode AND fallback failed). With fix #1 the fallback
   now actually reaches the backend on Windows, so the recovery completes.

Tests: apiBase.test.ts asserts Tauri → http://127.0.0.1:3900; the existing
playBlobAudioFallback.test.js (#653) still passes (fetch hits /preview/upload,
plays the HTTP URL, never a blob:).

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant