Skip to content

bug: ext_proc body parsing fails on chunked Transfer-Encoding requests #209

Description

@yossiovadia

Summary

The ext_proc handler fails to parse request bodies when clients send Transfer-Encoding: chunked. Two distinct failure modes observed, both traced to FULL_DUPLEX_STREAMED request body mode interacting with chunked TE:

  1. Framing contamination — chunked TE hex/CRLF bytes leak into body → "invalid character 'a' looking for beginning of value"
  2. Cross-stage truncation — when two ext_proc filters are chained, the second filter receives a truncated body → "unexpected end of JSON input"

Both are 100% reproducible with chunked TE. Neither occurs with Content-Length.

Root Cause

Why FULL_DUPLEX_STREAMED is used

The framework removed buffered mode in PR #38 ("remove buffered mode, use streaming only") and PR #169 added response chunk processing for streaming metering. FULL_DUPLEX_STREAMED is needed for responses (SSE streaming from providers), but requests don't need streaming — the framework accumulates the full request body in memory before parsing (server.go:162-168).

The EnvoyFilter sets FULL_DUPLEX_STREAMED for both directions because the framework is streaming-only. However, Envoy's ext_proc processing_mode allows setting request and response body modes independently.

What goes wrong

When request_body_mode: FULL_DUPLEX_STREAMED and the client sends Transfer-Encoding: chunked:

Bug 1 (single filter): Envoy passes raw chunked framing bytes into ext_proc body chunks. The accumulated body contains a0f\r\n{"model":"gpt-5.5"...}\r\n0\r\n\r\n instead of clean JSON. json.Unmarshal at request.go:67 fails.

Bug 2 (chained filters): Cross-stage log tracing with a single x-request-id shows:

  • payload-pre-processing: receives ~170 body chunks → reassembles correctly → parses model: gpt-5.5
  • payload-processing: receives one truncated chunk with EoS:trueunexpected end of JSON input

The first filter gets the complete body; the second filter gets a truncated copy. The context canceled on the first filter is an effect (stage 2 fails → Envoy tears down the chain), not the cause.

Reproduction

# Generate a test body (~43KB)
python3 -c "
import json
msgs = [{'role':'user','content':'analyze this: ' + 'x'*500}]
for i in range(20):
    msgs.append({'role':'assistant','content':[{'type':'output_text','text':'result ' + 'y'*2000}]})
    msgs.append({'role':'user','content':'check function ' + str(i)})
json.dump({'model':'gpt-5.5','input':msgs}, open('body.json','w'))
"

# FAILS — 100% failure rate (chunked TE)
curl -T body.json -H "Transfer-Encoding: chunked" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  https://<gateway>/v1/responses

# WORKS — 100% success rate (Content-Length)
curl -d @body.json \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  https://<gateway>/v1/responses

Production frequency: ~7 occurrences per 48h with Claude Code / Codex clients, auto-retried.

Suggested Fix

Primary fix (EnvoyFilter / operator): Change request_body_mode from FULL_DUPLEX_STREAMED to BUFFERED in the EnvoyFilter. Envoy then de-chunks and buffers the complete request body before sending to ext_proc, fixing both symptoms. Response body mode stays FULL_DUPLEX_STREAMED for SSE streaming — the two modes are independent per the ext_proc spec.

The framework already accumulates the full request body in memory before parsing (server.go:162-168), so BUFFERED matches the actual behavior. The pre-processing filter (read-only model extraction) especially has no need for request streaming.

The EnvoyFilter is generated by the MaaS operator (maas.opendatahub.io/v1alpha1 Config/default), so the one-line change belongs in the operator's kustomize manifests (deployment/base/payload-processing/manager/envoy-filter.yaml in opendatahub-io/models-as-a-service).

Defensive fix (framework, PR #210): tryDecodeChunked() in HandleRequestBody detects and strips chunked TE framing before json.Unmarshal. This fixes bug 1 (framing contamination) for any deployment using FULL_DUPLEX_STREAMED, but cannot fix bug 2 (truncated body — the bytes are genuinely missing). Validated on cluster: 15/15 chunked TE requests pass with the framework fix.

Environment

  • llm-d-inference-payload-processor v0.1.0-rc.3
  • Envoy ext_proc with request_body_mode: FULL_DUPLEX_STREAMED
  • Two chained ext_proc filters: payload-pre-processing + payload-processing
  • Observed with OpenAI Responses API (/v1/responses) and Anthropic Messages API (/v1/messages)
  • Clients: Codex CLI, Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions