feat(httputil): decode compressed request bodies (zstd/gzip/deflate)

Codex CLI 0.125+ defaults to sending request bodies with
Content-Encoding: zstd. Without server-side decompression the gateway
returns 'Failed to parse request body' on /v1/responses (and any other
JSON endpoint) because gjson sees raw zstd bytes.

ReadRequestBodyWithPrealloc now inspects Content-Encoding and
transparently decodes zstd, gzip/x-gzip, and deflate bodies before
returning them, then strips the encoding headers and updates
ContentLength so downstream code can reuse the bytes safely.
Unsupported encodings produce a clear error.

Adds unit tests covering identity, zstd, gzip, deflate, unsupported
encoding, corrupt zstd payloads, nil bodies, and explicit identity.
This commit is contained in:
Hai Chang
2026-04-26 16:48:30 +10:00
parent c056db740d
commit 798fd673e9
2 changed files with 198 additions and 2 deletions

View File

@@ -2,8 +2,15 @@ package httputil
import (
"bytes"
"compress/gzip"
"compress/zlib"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/klauspost/compress/zstd"
)
const (
@@ -11,7 +18,9 @@ const (
requestBodyReadMaxInitCap = 1 << 20
)
// ReadRequestBodyWithPrealloc reads request body with preallocated buffer based on content length.
// ReadRequestBodyWithPrealloc reads request body with preallocated buffer based
// on content length, transparently decoding any Content-Encoding the upstream
// client used to compress the body (zstd, gzip, deflate).
func ReadRequestBodyWithPrealloc(req *http.Request) ([]byte, error) {
if req == nil || req.Body == nil {
return nil, nil
@@ -33,5 +42,49 @@ func ReadRequestBodyWithPrealloc(req *http.Request) ([]byte, error) {
if _, err := io.Copy(buf, req.Body); err != nil {
return nil, err
}
return buf.Bytes(), nil
raw := buf.Bytes()
enc := strings.ToLower(strings.TrimSpace(req.Header.Get("Content-Encoding")))
if enc == "" || enc == "identity" {
return raw, nil
}
decoded, err := decompressRequestBody(enc, raw)
if err != nil {
return nil, fmt.Errorf("decode Content-Encoding %q: %w", enc, err)
}
req.Header.Del("Content-Encoding")
req.Header.Del("Content-Length")
req.ContentLength = int64(len(decoded))
return decoded, nil
}
func decompressRequestBody(encoding string, raw []byte) ([]byte, error) {
switch encoding {
case "zstd":
dec, err := zstd.NewReader(bytes.NewReader(raw))
if err != nil {
return nil, err
}
defer dec.Close()
return io.ReadAll(dec)
case "gzip", "x-gzip":
gr, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
return nil, err
}
defer gr.Close()
return io.ReadAll(gr)
case "deflate":
zr, err := zlib.NewReader(bytes.NewReader(raw))
if err != nil {
return nil, err
}
defer zr.Close()
return io.ReadAll(zr)
default:
return nil, errors.New("unsupported Content-Encoding")
}
}