Skip to content

fix(transcription): send OpenAI uploads through requestUrl - #514

Merged
chhoumann merged 2 commits into
masterfrom
fix/transcription-cors
Oct 19, 2026
Merged

chhoumann merged 2 commits into
masterfrom
fix/transcription-cors

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Why

On desktop, every OpenAI transcription, plain or diarized, failed with "Connection error." OpenAI sends its error responses without an Access-Control-Allow-Origin header for app://obsidian.md. Chromium blocked them from the renderer's fetch, so users never saw the real cause, such as a bad key or no quota.

What changed

  • createTranscription posts the same multipart bytes through requestWithTimeout, which uses Obsidian's requestUrl and has no CORS check. It accepts every status, so OpenAI's message and Retry-After reach the existing error mapping and retries.
  • requestWithTimeout joins a header that requestUrl returns as an array. Desktop returns Set-Cookie that way, and OpenAI always sets a Cloudflare cookie, so the wrapper rejected every OpenAI response as "Network response is invalid."
  • When every chunk of a plain transcription fails, the notice ends with the last chunk's error. It no longer says "Transcription failed:" twice.

Scope

  • I could not run a successful OpenAI transcription because this host has no OpenAI key. The success path ran end to end through requestUrl against the verify skill's fixture provider and the e2e journey stub.
  • requestUrl cannot cancel an upload. Cancel and the 10-minute timeout stop waiting and drop the late response, as streaming downloads do. The retry after a timeout still starts while the first upload runs. That is left for a follow-up.
  • A native error that mentions a timeout now reads "Connection error.", because the wrapper redacts transport errors.

Blast Radius

OpenAI transcription and diarization on every platform. The header change also reaches episode downloads. On desktop, a download from a host that sets a cookie should fail today with "Network response is invalid.", and this fixes it. That is inferred from the code path, not reproduced.

Verification

  • Isolated Obsidian 1.14.4 on Linux, an invalid key, the real OpenAI API, and Transcribe current episode from audio on a 60 s local WAV. Before, the plain notice read Transcription failed: Transcription failed: all 1 audio chunk(s) failed or returned no text., the diarized one ended in Connection error., and DevTools logged the CORS block on each of the three sends. After, both notices end with 401 Incorrect API key provided: sk-trx51*********-key. ..., and DevTools logs nothing.
  • npm run test:e2e -- tests/e2e/journey-resilience.test.ts -t "J-TR-0[234]" passes. J-TR-02 checks the SHA-256 of the file parsed back from the requestUrl body.
  • npm run lint, format:check, typecheck, build, test (2,782 tests), docs:build and test:tooling pass. npm run size reports 101,575 bytes gzip, against 101,457 on master and a budget of 103,000.

Each of these mutants fails at least one test:

  • Master's fetch client: 29 of 34 client and provider tests.
  • No acceptedStatuses, the 2xx-only default that Deepgram uses: the 401, 429, 500, 502 and 503 tests.
  • Abort reported as "Connection error.": the abort test.
  • Timeout reported as "Connection error.": the 10-minute timeout test.
  • Plain notice without the last error: the Whisper failure tests.
  • Set-Cookie arrays still rejected: the requestWithTimeout Set-Cookie test.
Plain Diarized
Before before plain before diarized
After after plain after diarized

Docs at release

None.

Fixes #513


Note

High Risk
Changes the network path for all OpenAI transcription and diarization plus shared requestWithTimeout header handling, affecting paid transcription on every platform and episode downloads that receive array Set-Cookie headers.

Overview
Fixes desktop OpenAI transcription failing with a generic "Connection error." by routing Whisper and diarized uploads through Obsidian's requestUrl (via requestWithTimeout) instead of renderer fetch, so CORS-blocked responses and real API errors (401, 429, etc.) surface correctly.

copyResponseHeaders now joins Set-Cookie when Obsidian returns it as a string array, which previously made every OpenAI response invalid. createTranscription accepts all HTTP statuses for manual error mapping and keeps retry/Retry-After behavior.

When every audio chunk fails, failure notices append the last chunk error and drop the duplicated "Transcription failed:" prefix. Tests, e2e journey stubs, and the verify net.mjs fixture were updated to mock requestUrl instead of fetch for OpenAI.

Reviewed by Cursor Bugbot for commit e4b6bc9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Route OpenAI transcription uploads through requestUrl in createTranscription

  • Replaces the renderer fetch upload in openaiTranscription.ts with requestWithTimeout, enforcing the 25 MiB body limit and a ten-minute timeout, and decoding binary responses before parsing JSON
  • TranscriptionService.transcribeChunks now tracks the final chunk error so transcribeEpisode reports the last failure detail when no usable transcript is produced
  • copyResponseHeaders in networkRequest.ts now accepts array-shaped string headers (e.g. Set-Cookie), joining values with commas
  • Rewrites tests across transcription, diarization, and journey suites to mock OpenAI through a shared requestUrl mock (openaiRequestUrl.ts) instead of fetch
  • Behavioral Change: a timeout or caller abort now stops waiting for the upload instead of cancelling it — requestUrl cannot cancel an in-flight upload, and abort tests no longer assert a cancelled fetch signal. Failure messages for exhausted chunks now include the last error detail

Macroscope summarized e4b6bc9.

The transcription tests now answer OpenAI through Obsidian's requestUrl and
reject fetch the way Chromium does when OpenAI's response has no
Access-Control-Allow-Origin header for app://obsidian.md. A plain
transcription that fails on every chunk must show the last API error, and
requestWithTimeout must accept the Set-Cookie array that the desktop
requestUrl returns. The e2e journeys stub OpenAI on the request-url IPC
channel instead of fetch.

These tests fail until the next commit.
OpenAI answers errors without an Access-Control-Allow-Origin header for
app://obsidian.md. Chromium blocked those responses from the renderer's
fetch, so every failed upload, plain or diarized, showed "Connection
error." and hid the real cause, such as a bad key or no quota.

createTranscription now posts the same multipart bytes through
requestWithTimeout and accepts every status, so OpenAI's message and
Retry-After reach the caller. A timeout or abort stops waiting and drops
the late response, because requestUrl cannot cancel an upload.

requestWithTimeout rejected every response that set a cookie, because the
desktop requestUrl returns Set-Cookie as an array. It now joins string
arrays with ", ". A plain transcription that fails on every chunk now
ends its notice with the last chunk's error and no longer repeats
"Transcription failed:".

The verify skill's fixture provider now answers OpenAI on requestUrl.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: e4b6bc9
Status: ✅  Deploy successful!
Preview URL: https://56386c4b.podnotes.pages.dev
Branch Preview URL: https://fix-transcription-cors.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit d97fad1 into master Oct 19, 2026
21 checks passed
@chhoumann
chhoumann deleted the fix/transcription-cors branch October 19, 2026 06:29
@ManuelVicenteG

Copy link
Copy Markdown

Hi Christian,

Thanks for the quick fix and your great work on PodNotes! I really appreciate it and look forward to testing the next release.

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.

Bug: OpenAI transcription fails with CORS error in PodNotes 2.22.0

2 participants