openclaw - ✅(Solved) Fix Audio transcription silently fails with HTTP 400 when using undici's fetch dispatcher (FormData Content-Type not set) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#63201Fetched 2026-04-09 07:57:05
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Participants
Timeline (top)
cross-referenced ×1

After fixing the SSRF block (#63132 / #63147), audio transcription still silently fails for self-hosted STT providers. The server receives a malformed multipart body and returns HTTP 400 `{"error":"No file part in the request"}`. No error is visible to the user — the transcript comes back as `undefined` and the gateway falls back to `"I didn't catch that — please type your message."`

Error Message

After fixing the SSRF block (#63132 / #63147), audio transcription still silently fails for self-hosted STT providers. The server receives a malformed multipart body and returns HTTP 400 `{"error":"No file part in the request"}`. No error is visible to the user — the transcript comes back as `undefined` and the gateway falls back to `"I didn't catch that — please type your message."` The error is silently swallowed in `runAttachmentEntries` (`logVerbose` only), making this extremely hard to diagnose. This affects any OpenAI-compatible audio transcription endpoint, not just private-network ones. The SSRF fix (#63147) is a prerequisite to even reaching this code path for LAN providers, but the FormData bug is a separate issue that would affect public endpoints too if they returned a helpful error (many just hang or return a different error).

Root Cause

`transcribeOpenAiCompatibleAudio` builds the request body using the Web API's `new FormData()` (Node.js `globalThis.FormData`). However, the audio transcription code path routes through `fetchWithRuntimeDispatcher`, which calls undici's npm package `fetch` (not Node.js's bundled `globalThis.fetch`).

undici's npm `fetch` does not handle `globalThis.FormData` correctly: when a `FormData` body is dispatched through a custom undici dispatcher (created by `createPinnedDispatcher` for DNS pinning), the `Content-Type: multipart/form-data; boundary=...` header is never set. The request body arrives at the server with no parseable multipart boundary, so form fields — including the `file` field — are invisible to the server.

Confirmed with `undici` npm v8.0.2 + Node.js v24.14.0:

```js // Works — Node.js built-in fetch handles globalThis.FormData correctly globalThis.fetch(url, { method: "POST", body: formData }) // ✅

// Fails — undici npm fetch does not import { fetch } from "undici"; fetch(url, { method: "POST", body: formData }) // ❌ HTTP 400 "No file part in the request" ```

The error is silently swallowed in `runAttachmentEntries` (`logVerbose` only), making this extremely hard to diagnose.

Fix Action

Fix

Replace `new FormData()` + `Blob` with a manually-constructed `Uint8Array` multipart body and an explicit `Content-Type` header. This works correctly with any fetch implementation — undici npm, undici via dispatcher, or globalThis.fetch.

See PR #[to be linked] for implementation and tests.

PR fix notes

PR #63202: fix: build multipart body manually in transcribeOpenAiCompatibleAudio

Description (problem / solution / changelog)

Problem

`transcribeOpenAiCompatibleAudio` uses `new FormData()` (Node.js `globalThis.FormData`) to build the audio upload body. Audio transcription requests are dispatched through `fetchWithRuntimeDispatcher`, which uses undici's npm package `fetch` with a custom dispatcher for DNS pinning. undici's npm `fetch` does not handle `globalThis.FormData` — the `Content-Type: multipart/form-data; boundary=...` header is never set, so the server receives an unparseable body:

``` HTTP 400: {"error": "No file part in the request"} ```

This error is caught by `runAttachmentEntries` and logged only at `logVerbose` level. From the user's perspective, the transcript silently comes back as `undefined` and the gateway says "I didn't catch that — please type your message."

Confirmed with undici npm v8.0.2 + Node.js v24.14.0. `globalThis.fetch` handles `globalThis.FormData` correctly; undici's npm `fetch` does not.

Closes #63201

Fix

Replace `FormData` / `Blob` with a manually-constructed `Uint8Array` multipart body and an explicit `Content-Type` header containing the boundary. This works correctly with any fetch implementation — undici npm, undici with a custom dispatcher, or `globalThis.fetch`.

```typescript // Before const form = new FormData(); form.append("file", new Blob([bytes], { type: mime }), fileName); form.append("model", model); // ... await postTranscriptionRequest({ body: form, headers, ... });

// After const boundary = ----FormBoundary${crypto.randomUUID().replace(/-/g, "")}; // ... build Uint8Array parts manually ... const headersWithCT = new Headers(headers); headersWithCT.set("content-type", multipart/form-data; boundary=${boundary}); await postTranscriptionRequest({ body: Buffer.from(body), headers: headersWithCT, ... }); ```

Testing

Three new tests in `openai-compatible-audio.test.ts`:

  1. Confirms the request body is a `Buffer` (not `FormData`) and the `Content-Type` header is `multipart/form-data; boundary=...`
  2. Confirms all four fields (`file`, `model`, `language`, `prompt`) are correctly encoded in the body
  3. Confirms optional `language` and `prompt` fields are omitted when not provided

All 5 tests pass (2 existing + 3 new).

Affected paths

  • `src/media-understanding/openai-compatible-audio.ts` — replace FormData with manual multipart
  • `src/media-understanding/openai-compatible-audio.test.ts` — three new test cases

🤖 Generated with Claude Code

Changed files

  • extensions/openai/media-understanding-provider.test.ts (modified, +17/-13)
  • src/media-understanding/openai-compatible-audio.test.ts (modified, +150/-0)
  • src/media-understanding/openai-compatible-audio.ts (modified, +47/-11)
RAW_BUFFERClick to expand / collapse

Summary

After fixing the SSRF block (#63132 / #63147), audio transcription still silently fails for self-hosted STT providers. The server receives a malformed multipart body and returns HTTP 400 `{"error":"No file part in the request"}`. No error is visible to the user — the transcript comes back as `undefined` and the gateway falls back to `"I didn't catch that — please type your message."`

Root Cause

`transcribeOpenAiCompatibleAudio` builds the request body using the Web API's `new FormData()` (Node.js `globalThis.FormData`). However, the audio transcription code path routes through `fetchWithRuntimeDispatcher`, which calls undici's npm package `fetch` (not Node.js's bundled `globalThis.fetch`).

undici's npm `fetch` does not handle `globalThis.FormData` correctly: when a `FormData` body is dispatched through a custom undici dispatcher (created by `createPinnedDispatcher` for DNS pinning), the `Content-Type: multipart/form-data; boundary=...` header is never set. The request body arrives at the server with no parseable multipart boundary, so form fields — including the `file` field — are invisible to the server.

Confirmed with `undici` npm v8.0.2 + Node.js v24.14.0:

```js // Works — Node.js built-in fetch handles globalThis.FormData correctly globalThis.fetch(url, { method: "POST", body: formData }) // ✅

// Fails — undici npm fetch does not import { fetch } from "undici"; fetch(url, { method: "POST", body: formData }) // ❌ HTTP 400 "No file part in the request" ```

The error is silently swallowed in `runAttachmentEntries` (`logVerbose` only), making this extremely hard to diagnose.

Impact

This affects any OpenAI-compatible audio transcription endpoint, not just private-network ones. The SSRF fix (#63147) is a prerequisite to even reaching this code path for LAN providers, but the FormData bug is a separate issue that would affect public endpoints too if they returned a helpful error (many just hang or return a different error).

Configuration

```json "tools": { "media": { "audio": { "enabled": true, "echoTranscript": true, "models": [ { "provider": "openai", "model": "parakeet", "baseUrl": "http://192.168.30.208:5092/v1" } ] } } } ```

Fix

Replace `new FormData()` + `Blob` with a manually-constructed `Uint8Array` multipart body and an explicit `Content-Type` header. This works correctly with any fetch implementation — undici npm, undici via dispatcher, or globalThis.fetch.

See PR #[to be linked] for implementation and tests.

Related

  • SSRF fix (prerequisite for LAN providers): #63132, #63147
  • Allowlist fix: #62205

extent analysis

TL;DR

Replace new FormData() with a manually-constructed Uint8Array multipart body and an explicit Content-Type header to fix the audio transcription issue.

Guidance

  • Identify the code path using fetchWithRuntimeDispatcher and undici's fetch to determine where the FormData issue is occurring.
  • Verify that the Content-Type header is not being set correctly by checking the request headers in the network logs or using a debugging tool.
  • Consider implementing a manual multipart body construction as described in the fix to ensure compatibility with different fetch implementations.
  • Review the related issues (#63132, #63147, #62205) to ensure that all prerequisites and related fixes are applied.

Example

// Manual multipart body construction example
const boundary = '---------------------------boundary';
const contentType = `multipart/form-data; boundary=${boundary}`;
const requestBody = new Uint8Array([
  // Form field data
  Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="audio.wav"\r\nContent-Type: audio/wav\r\n\r\n`),
  // Audio data
  audioBuffer,
  // Form field data
  Buffer.from(`\r\n--${boundary}--\r\n`),
]);

// Set explicit Content-Type header
fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': contentType,
  },
  body: requestBody,
});

Notes

The provided fix and example assume that the audio data is available as a Uint8Array or can be converted to one. Additionally, the boundary and content type headers may need to be adjusted based on the specific requirements of the audio transcription endpoint.

Recommendation

Apply the workaround by replacing new FormData() with a manually-constructed Uint8Array multipart body and an explicit Content-Type header, as this ensures compatibility with different fetch implementations and fixes the audio transcription issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING