2026-02-28 15:01:20 +08:00
|
|
|
package httputil
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
"compress/gzip"
|
|
|
|
|
"compress/zlib"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-02-28 15:01:20 +08:00
|
|
|
"io"
|
|
|
|
|
"net/http"
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/klauspost/compress/zstd"
|
2026-02-28 15:01:20 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
requestBodyReadInitCap = 512
|
|
|
|
|
requestBodyReadMaxInitCap = 1 << 20
|
2026-04-29 22:11:45 +08:00
|
|
|
// maxDecompressedBodySize limits the decompressed request body to 64 MB
|
|
|
|
|
// to prevent decompression bomb attacks.
|
|
|
|
|
maxDecompressedBodySize = 64 << 20
|
2026-02-28 15:01:20 +08:00
|
|
|
)
|
|
|
|
|
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
// 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).
|
2026-02-28 15:01:20 +08:00
|
|
|
func ReadRequestBodyWithPrealloc(req *http.Request) ([]byte, error) {
|
|
|
|
|
if req == nil || req.Body == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
capHint := requestBodyReadInitCap
|
|
|
|
|
if req.ContentLength > 0 {
|
|
|
|
|
switch {
|
|
|
|
|
case req.ContentLength < int64(requestBodyReadInitCap):
|
|
|
|
|
capHint = requestBodyReadInitCap
|
|
|
|
|
case req.ContentLength > int64(requestBodyReadMaxInitCap):
|
|
|
|
|
capHint = requestBodyReadMaxInitCap
|
|
|
|
|
default:
|
|
|
|
|
capHint = int(req.ContentLength)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, capHint))
|
|
|
|
|
if _, err := io.Copy(buf, req.Body); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
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()
|
2026-04-29 22:11:45 +08:00
|
|
|
return io.ReadAll(io.LimitReader(dec, maxDecompressedBodySize))
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
case "gzip", "x-gzip":
|
|
|
|
|
gr, err := gzip.NewReader(bytes.NewReader(raw))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-29 22:11:45 +08:00
|
|
|
defer func() { _ = gr.Close() }()
|
|
|
|
|
return io.ReadAll(io.LimitReader(gr, maxDecompressedBodySize))
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
case "deflate":
|
|
|
|
|
zr, err := zlib.NewReader(bytes.NewReader(raw))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-29 22:11:45 +08:00
|
|
|
defer func() { _ = zr.Close() }()
|
|
|
|
|
return io.ReadAll(io.LimitReader(zr, maxDecompressedBodySize))
|
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.
2026-04-26 16:48:30 +10:00
|
|
|
default:
|
|
|
|
|
return nil, errors.New("unsupported Content-Encoding")
|
|
|
|
|
}
|
2026-02-28 15:01:20 +08:00
|
|
|
}
|