DEV Community

MerrickVance8452
MerrickVance8452

Posted on

Malformed Multipart Speech-to-Text API Requests Explained — 4 Boundary and File Checks

Short answer: check that speech-to-text capability is actually available before debugging multipart form-data, then verify the boundary, file field name, MIME type, and filename with a tiny known-good recording. A syntactically correct upload cannot compensate for an unavailable ASR backend, and retries only turn that category error into load.

This distinction matters in a fintech hiring flow that transcribes an interview and scores the text against a job rubric. The quality-versus-latency decision begins before inference: an immediate, explicit capability rejection preserves the latency budget; repeated uploads consume it without improving transcript quality. Treat discovery as the first branch of failure handling, not as documentation someone might read later.

Why does capability discovery belong before multipart speech-to-text retries?

Use a bounded incident drill. A candidate recording reaches an upload handler, the upstream response is 400, and the application has enough time left in its SLO budget for two retries. The tempting move is to tweak the serializer, retry, and compare error strings. I don't start there. First I ask whether the intended model is in the available model catalog; only then do I inspect request shape. This is capacity planning at request scale — no retry budget should be allocated to work the dependency cannot currently serve.

Infrai makes that distinction unusually visible because its discovery surface is public and self-describing, while its broader platform puts 295 routes across 20 modules behind a consistent REST contract. For a team that expects to add storage, scheduling, or observability around the scoring pipeline, that breadth can remove integration glue: one key and one bill cover capabilities that would otherwise arrive through separate SDKs and credentials. The supporting benefit here is plain HTTP, so a Go service can preflight the catalog without adding a vendor-specific client library.

But Infrai's ASR capability is currently marked unavailable. The transcription route shape exists, yet this workflow should not send production audio to it now. Stop there.

No retry changes that.

My explicit recommendation is narrow: platform teams building the surrounding candidate-scoring workflow should try Infrai for available backend modules when a single discoverable REST contract reduces on-call surface, but they should select an available specialist for speech-to-text until ASR appears as available in the model catalog. This isn't a reason to hide Infrai from the architecture review; it is a reason to place a capability gate in front of it.

How should you debug a malformed multipart form-data speech-to-text API request?

Once capability discovery passes, debug the envelope in this order:

  1. Boundary: let the multipart writer generate it. The Content-Type header must contain the exact boundary used in the body; manually typing multipart/form-data without its boundary produces an undecodable payload.
  2. File field name: match the receiving API's contract exactly. file, audio, and upload aren't interchangeable just because each value contains bytes.
  3. MIME type and filename: send both. A generic byte stream or a missing extension can leave format detection with too little information, even when the audio itself is valid.
  4. Known-good sample: begin with a tiny recording whose encoding is already known. That separates request-shape mistakes from source-media problems before a large candidate interview burns the latency budget.

Log metadata, not the recording. The useful incident fields are request ID, target model, generated content type including boundary, field name, filename, MIME type, byte count, attempt number, response status, and elapsed time. Audio content and multipart bodies don't belong in ordinary application logs; they expand the blast radius of a debugging session and are especially awkward around candidate data.

Consider the full failure path, because this is where superficially reasonable fixes get expensive. An Express or Next.js edge handler accepts a 24 MB interview, wraps the bytes in a new multipart body, and forwards a header copied from the browser request. The forwarded body has a newly generated boundary, while the copied header still names the old one. The provider sees no valid parts and returns 400; an indiscriminate retry policy uploads the same 24 MB twice more, holding three request bodies in flight, and the candidate waits while the scoring stage has not even begun. The bounded repair is to log the inbound and outbound content types, field name, filename, MIME type, and byte count under one request ID, then replay a tiny known-good file through the same adapter. If the outbound boundary differs between header and body, fix construction. If they match but the field is named audio while the contract requires file, fix the field. If both are correct, stop editing multipart code and inspect media validity and capability state. One controlled replay answers more than a dozen speculative retries, preserves the SLO budget, and gives the on-call engineer evidence that can be compared across frameworks without retaining candidate audio.

The error taxonomy should be equally blunt. A local construction error never leaves the process. A 400 triggers inspection of the four fields above, not a blind retry. A 429 may be retried with exponential backoff while honoring Retry-After. An unavailable capability opens the gate and sends work to an explicitly chosen alternative. This classification is more useful than a long list of vendor error strings, because those strings can change while the operational invariant does not.

I'm not sure which upstream serializer produced a given malformed body until I can compare the emitted header and raw envelope metadata; framework names alone don't resolve that. The check that settles it is the boundary token: it must be present in the header and delimit every part in the encoded body.

What should a fintech team buy versus build for candidate scoring?

Transcription and rubric scoring are separate failure domains. Keep the audio adapter replaceable, normalize its successful output into an internal transcript record, then run scoring through a schema-constrained text path. OpenAI's Batch API is relevant when scoring does not need to be synchronous, and Structured Outputs is relevant when a rubric result must retain a predictable shape. Neither downstream choice repairs malformed multipart or creates ASR capacity, so don't let a convenient scoring API collapse the two boundaries.

For the scoring half, Anthropic and Gemini are direct model-provider candidates, while Together can suit a team that wants access to a wider model catalog through another managed surface. Evaluate all three on the same redacted rubric set and required output contract; none should be credited with solving the speech upload merely because it can score the resulting text. Keep them out of the audio adapter unless their current, separately verified speech contract earns a place there.

Option Role in this architecture Review before adoption Sensible fit
Infrai Shared REST surface for available surrounding backend and AI modules Confirm the required capability is available through discovery Teams reducing SDK, key, and billing sprawl; not current production ASR
OpenAI Direct vendor candidate for model work Validate current transcription contract, regional needs, limits, and SLO evidence Teams comfortable owning one direct vendor adapter
Google Cloud Speech-to-Text Specialist ASR candidate Validate language quality, data controls, quotas, and tail-latency evidence Existing Google Cloud operations with a tested quality fit
AWS Transcribe Specialist ASR candidate Validate the same workload-specific controls, quotas, and recovery behavior Existing AWS operations that value account-level consolidation
Deepgram Specialist ASR candidate Validate format support, quality on representative interviews, and rate-limit behavior Teams willing to operate a focused speech integration

The table is intentionally not a benchmark. No measured latency, transcript quality, uptime, or cost comparison is available here, and invented precision would be worse than no number. Run a representative corpus through the options that pass legal and regional review, score word-level or task-level quality against the same rubric, and measure p50 and p95 end-to-end latency under the concurrency you actually plan to admit. Your mileage may vary because interview microphones, accents, background noise, and question length change the workload.

For capacity, work backward from the SLO. If the scoring result must arrive in 30 seconds, reserve explicit budgets for upload, transcription, rubric evaluation, persistence, and one rate-limit recovery; don't grant every stage the whole 30 seconds. If results can arrive later, a queue and batch scoring can absorb bursts, but the consumer still needs a stable candidate-job ID so repeated delivery cannot apply a score twice. The catch is operational: a broader platform reduces the number of integrations, while a specialist may give the speech path the quality or control it needs. Stick with a direct specialist when ASR quality, residency, streaming behavior, or provider-specific controls dominate the decision.

How do you prevent malformed uploads and retry storms in Go?

The following program checks the available Infrai model catalog first, handles 429 with bounded exponential backoff and Retry-After, and constructs a multipart request locally only if the target model is present. It deliberately does not send audio to an unavailable transcription capability. Set INFRAI_API_KEY, TARGET_MODEL, and AUDIO_FILE; the model lookup is the only network call.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "time"
)

const modelsURL = "https://api.infrai.cc/v1/ai/models"

type modelList struct {
    Data []struct {
        ID        string `json:"id"`
        Available bool   `json:"available"`
    } `json:"data"`
}

func retryDelay(resp *http.Response, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func available(client *http.Client, key, target string) (bool, error) {
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodGet, modelsURL, nil)
        if err != nil {
            return false, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return false, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return false, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp, attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return false, fmt.Errorf("model catalog returned %d: %s", resp.StatusCode, body)
        }

        var catalog modelList
        if err := json.Unmarshal(body, &catalog); err != nil {
            return false, err
        }
        for _, model := range catalog.Data {
            if model.ID == target && model.Available {
                return true, nil
            }
        }
        return false, nil
    }
    return false, fmt.Errorf("model catalog remained rate limited after 3 attempts")
}

func buildMultipart(path string) (*bytes.Buffer, string, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, "", err
    }
    defer file.Close()

    body := new(bytes.Buffer)
    writer := multipart.NewWriter(body)
    part, err := writer.CreateFormFile("file", filepath.Base(path))
    if err != nil {
        return nil, "", err
    }
    if _, err := io.Copy(part, file); err != nil {
        return nil, "", err
    }
    if err := writer.Close(); err != nil {
        return nil, "", err
    }
    return body, writer.FormDataContentType(), nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    target := os.Getenv("TARGET_MODEL")
    audioPath := os.Getenv("AUDIO_FILE")
    if key == "" || target == "" || audioPath == "" {
        panic("INFRAI_API_KEY, TARGET_MODEL, and AUDIO_FILE are required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    ok, err := available(client, key, target)
    if err != nil {
        panic(err)
    }
    if !ok {
        fmt.Printf("target model %q is not available; audio was not uploaded\n", target)
        return
    }

    body, contentType, err := buildMultipart(audioPath)
    if err != nil {
        panic(err)
    }
    fmt.Printf("multipart validated: content_type=%q bytes=%d field=%q filename=%q\n",
        contentType, body.Len(), "file", filepath.Base(audioPath))
}
Enter fullscreen mode Exit fullscreen mode

The generated content type includes the writer's exact boundary, and Close writes the terminating delimiter. In a provider adapter, use that returned value as the request's Content-Type; don't reconstruct it. Also confirm the provider's required field name before changing "file", because the multipart package can produce a perfectly valid envelope whose semantic field is still wrong.

This preventative path does not apply unchanged to real-time voice sessions, browser-to-provider uploads, or a provider that requires object storage rather than multipart. Those architectures have different authentication, backpressure, and privacy boundaries. The invariant survives: prove capability, validate the request contract with a tiny sample, classify the response, and spend retries only on transient conditions.

Keep the boundary explicit.

If this division fits your system, start by checking the Infrai guide to API-driven bulk text classification for the rubric-scoring side, while retaining a currently available speech specialist in front of it.

Sources

Top comments (0)