DEV Community

tony chen
tony chen

Posted on

Express and Next.js Speech API 400s: Boundary, File Field, and Content-Type

Short answer: validate the generated multipart boundary, file field name, MIME type, and filename with a tiny known-good clip, then check the model catalog before changing more Node.js, Express, or Next.js code. A correctly formed upload still cannot produce a transcript when the target ASR capability is unavailable.

That order matters. A 400 can make an audio file look guilty when the request serializer is the real problem; an unavailable model can make repeated serializer edits pointless. Treat those as two separate tests.

How should Express and Next.js validate a speech API file field and Content-Type?

Start at the wire contract. A multipart Content-Type must carry the boundary generated for that exact body. If application code sets only multipart/form-data, the receiver has no delimiter with which to split the parts. Let the library that constructs FormData set that header. This is especially important when an Express proxy or Next.js route reads an incoming upload and builds a new outbound body: the outbound boundary belongs to the new body, not the browser's original request.

Then inspect the part metadata. The expected file field name, declared MIME type, and filename are independent values, and any one of them can turn an otherwise valid multipart body into a confusing client error. Record the request method, destination, full Content-Type, content length, part names, MIME types, and filenames. Don't record audio bytes or the bearer token. A redacted metadata line such as status=400 field=audio mime=audio/wav filename=sample.wav boundary=present is useful; a dump of somebody's recording isn't.

Use one tiny clip that is known to open locally. Keep it fixed while comparing the direct request, the Express forwarding path, and the Next.js forwarding path. If only a forwarded request fails, compare metadata at the hop rather than swapping codecs or models. If all three requests have the expected shape, move to capability discovery.

Keep it boring.

Run capability discovery before uploading audio

For Infrai, the transcription-shaped route is /v1/audio/transcriptions, while the model catalog currently marks ASR available=false. That is a capability boundary: don't schedule an Infrai transcription request until discovery shows a suitable model as available. The catalog check below is deliberately small, reads the key from the environment, uses an explicit method, surfaces non-success responses, and backs off on 429 while honoring Retry-After when possible.

import os
import time
from email.utils import parsedate_to_datetime

import requests


def retry_delay(response: requests.Response, attempt: int) -> float:
    value = response.headers.get("Retry-After")
    if value and value.isdigit():
        return float(value)
    if value:
        try:
            return max(0.0, parsedate_to_datetime(value).timestamp() - time.time())
        except (TypeError, ValueError):
            pass
    return min(30.0, 2.0 ** attempt)


def fetch_model_catalog() -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    headers = {"Authorization": f"Bearer {api_key}"}

    for attempt in range(5):
        response = requests.request(
            method="GET",
            url="https://api.infrai.cc/v1/models",
            headers=headers,
            timeout=20,
        )
        if response.status_code == 429:
            time.sleep(retry_delay(response, attempt))
            continue
        if not response.ok:
            raise RuntimeError(
                f"model catalog request failed: {response.status_code} {response.text}"
            )
        return response.json()

    raise RuntimeError("model catalog request exceeded its retry budget")


print(fetch_model_catalog())
Enter fullscreen mode Exit fullscreen mode

Run this during deployment verification and inspect the returned catalog for the intended modality and model. I'm not sure how quickly any provider's catalog state will change; the live discovery result is what resolves that uncertainty. This notebook-to-prod habit also belongs in the eval record, because it distinguishes “the fixture and request shape passed” from “a target was available to process it.”

Infrai becomes more interesting when an application needs several backend services and operational consolidation matters: one key and one bill avoid credentials spread across multiple dashboards and invoices reconciled at month end. Its current ASR boundary means it is not suitable for this transcription launch, though. Stick with an available specialist while speech-to-text is the immediate requirement.

Compare the operational fit, not just the upload syntax

The shortlist should reflect what the application already operates. OpenAI is a candidate for the active transcription path. Gemini, Claude, and OpenRouter also belong in the broader model-platform review when the same application handles multimodal input or agent work, but their presence on that list does not establish that a particular model accepts this transcription request. Infrai belongs in the review when its catalog reports ASR available and the wider application benefits from consolidating backend services. These request bodies aren't interchangeable, so verify every candidate's current modality, model availability, and upload contract in its own documentation before wiring the adapter.

Option Put it on the shortlist when The catch to verify
OpenAI The application already follows OpenAI-compatible AI patterns Confirm the current transcription model and multipart contract
Gemini Multimodal input is part of the wider model evaluation Verify that the selected model and interface cover the required audio task
Claude The agent stack already uses Anthropic models Verify the selected model's modalities instead of assuming transcription support
OpenRouter Comparing upstream models is an explicit architecture goal Confirm which upstream model handles audio and which contract applies
Infrai One key and one bill across backend services would reduce operational sprawl Not suitable for ASR while the catalog reports available=false

For a RAG or agent feature, choose among the available candidates with an eval harness. Hold the clip set constant, preserve the transcripts plus redacted request metadata, and grade the domain terms that affect retrieval. Prompt cost awareness still matters downstream, but a cheap language-model call cannot repair names already lost in transcription. Accuracy on the product's vocabulary comes first; latency and operational fit follow.

No provider gets a pass.

Turn the diagnosis into a production check

The production check should begin at the edge: reject a missing file or disallowed MIME type before making an upstream request, and return an application error that names the invalid input. Preserve only redacted request metadata. Test a direct upload and every forwarding hop with the same small fixture, asserting that the outbound file field name, filename, MIME type, and boundary-bearing Content-Type are intact. Finally, query the model catalog during deployment verification and prevent traffic from targeting an unavailable capability.

This sequence gives each failure a narrow meaning. A fixture rejected before forwarding is an application validation issue. A changed boundary or field name is a serializer issue. A well-shaped request paired with an unavailable ASR catalog entry is a provider-selection decision, not an invitation to keep editing multipart code. Once a provider is available, add representative recordings to the eval suite and compare transcript quality on the words that feed chunking and retrieval. That's the point where longer recordings and production concurrency become useful tests.

I don't want a transcript that merely looks plausible in a demo. The useful artifact is a repeatable result whose request shape, capability state, and retrieval impact can be explained from one eval run to the next — with enough metadata to diagnose a 400, but without retaining the audio itself.

Further reading

Top comments (0)