Official transcription provider fails with "Failed to fetch" on desktop — api.airi.build CORS preflight rejects the packaged app's file:// origin
Describe the bug
Environment: AIRI v0.12.0-beta.5, Windows x64 installer, Windows 11 x64. Settings → Hearing (听觉) → Service source (服务来源) → AIRI 官方语音识别服务, model Auto. System proxy off; api.airi.build is directly reachable from this machine (all requests below responded in < 1 s), so DNS/firewall/connectivity is not the cause. Failure is independent of login state — the preflight fails before auth is ever evaluated.
Actual behavior
Start listening in the hearing test panel and the panel immediately (100% reproducible) shows:
转写错误 / Failed to fetch — check the browser console (Network tab) for the exact reason (e.g. CORS, network timeout, DNS failure).
The microphone itself is fine: the input level meter moves and "speech detected" lights up. The stream request is initiated as soon as listening starts, so the error appears regardless of whether anyone speaks.
Expected behavior
The official provider should establish its streaming request; without credentials it should surface a meaningful 401-based error, not a browser-level fetch failure.
Steps to reproduce
- Launch the packaged desktop app (v0.12.0-beta.5, Windows x64).
- Settings → 听觉 (Hearing) → 服务来源 (Service source) → select AIRI 官方语音识别服务; keep model Auto.
- In 听觉测试面板 click 开始监听 (Start listening).
- Observe the 转写错误 shown immediately (speaking not required).
Evidence
1. The endpoint is reachable and healthy — a plain POST gets the expected 401 (no credentials), and preflight returns 204:
$ curl -sS -D- -o /dev/null -X POST https://api.airi.build/api/v1/audio/transcriptions/stream
HTTP/1.1 401 Unauthorized
access-control-allow-credentials: true
server: cloudflare
2. The server reflects access-control-allow-origin per-origin (vary: Origin), but only dev origins are allowlisted — http://localhost:5173 is reflected; the packaged app's origin is not:
$ curl -sS -D- -o /dev/null -X OPTIONS "https://api.airi.build/api/v1/audio/transcriptions/stream" \
-H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type"
HTTP/1.1 204 No Content
access-control-allow-origin: http://localhost:5173 <-- reflected OK
$ # same request with -H "Origin: null" (what a file:// renderer sends):
HTTP/1.1 204 No Content
access-control-allow-credentials: true
access-control-allow-headers: authorization,content-type
access-control-allow-methods: GET,HEAD,PUT,POST,DELETE,PATCH,QUERY
vary: Origin, Access-Control-Request-Headers
<-- NO access-control-allow-origin
Same result for Origin: https://airi.build and Origin: app://airi — no access-control-allow-origin returned.
3. The desktop renderer runs from a file:// page (file:///…/resources/app.asar/out/renderer/index.html), so its fetch() carries Origin: null. The endpoint requires a preflight (POST + authorization/content-type), and the preflight above passes at HTTP level (204) but fails the CORS check (no matching access-control-allow-origin), so Chromium aborts the request and the app surfaces TypeError: Failed to fetch through its error-message wrapper.
4. Client code path — the official transcription provider builds its request in the renderer, subject to CORS:
SERVER_URL→packages/stage-ui/src/libs/server.ts, line 1 (tagv0.12.0-beta.5):import.meta.env.VITE_SERVER_URL || 'https://api.airi.build'- Provider registration →
packages/stage-ui/src/libs/providers/providers/official/index.ts, lines 368–376 (tagv0.12.0-beta.5):
createProvider(_config) {
return {
transcription: (model: string) => ({
baseURL: new URL(`${SERVER_URL}/api/v1/audio/transcriptions/stream`),
fetch: withCredentials(),
model,
}),
}
}
Root cause
The official provider's fetch runs in the packaged renderer whose origin is null (file://), but api.airi.build's CORS allowlist only reflects dev origins such as http://localhost:5173. The preflight therefore never passes in the packaged desktop app, so the AIRI official transcription service cannot work in it at all — independent of login state or network quality.
Suggested fixes (any one would do)
- Client: perform official-provider requests through the Electron main process (same approach as #2011 does for openai-compatible requests), bypassing renderer CORS.
- Server: allowlist the packaged app's origin — note a
file://page sends the literal stringnull, which is awkward to allowlist, so this likely requires shipping the renderer from a custom protocol origin (e.g.app://airi) and reflecting that. - Land #2290 (official ASR over WebSocket) — the WebSocket handshake is not subject to fetch CORS, which sidesteps the problem entirely.
Related
- #2290 — moves official ASR to WebSocket (would fix this as a side effect; currently unmerged)
- #2011 — main-process proxy for openai-compatible requests (currently unmerged)
- #1832 — separate problem: model-based VAD fails to load silero-vad from HuggingFace on CN networks. The "inference error" (推理错误) seen alongside belongs there, not here — beta.5 already falls back to volume detection, and the transcription error above still occurs.
System Info
System:
OS: Windows 11 10.0.26200
CPU: (12) x64 12th Gen Intel(R) Core(TM) i5-12400
Memory: 13.65 GB / 31.33 GB
Binaries:
Node: 26.3.0 - C:\Program Files\nodejs\node.EXE
npm: 11.16.0 - C:\Program Files\nodejs\npm.CMD
pnpm: 11.5.1 - C:\Users\Administrator\AppData\Roaming\npm\pnpm.CMD
Browsers:
Edge: Chromium (152.0.4191.53)
Firefox: 153.0.4 - C:\Program Files\Mozilla Firefox\firefox.exe
Validations
- Follow our Code of Conduct
- Read the Contributing Guide.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
Contributions
- I am willing to submit a PR to fix this issue
- I am willing to submit a PR with failing tests (actually just go ahead and do it, thanks!)
Source: moeru-ai/airi