Skip to content

Commit 847c250

Browse files
committed
feat(runtime): add Brotli and Zstd compression support, improve response handling - Implemented Brotli and Zstd decompression handling in `FileRequestLogger` and executor logic for enhanced compatibility. - Added `decodeResponseBody` utility for streamlined multi-encoding support (Gzip, Deflate, Brotli, Zstd). - Improved resource cleanup with composite readers for proper closure under all conditions. - Updated dependencies in `go.mod` and `go.sum` to include Brotli and Zstd libraries.
1 parent c7196ba commit 847c250

File tree

4 files changed

+178
-43
lines changed

4 files changed

+178
-43
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
cloud.google.com/go/compute/metadata v0.3.0 // indirect
2929
github.com/Microsoft/go-winio v0.6.2 // indirect
3030
github.com/ProtonMail/go-crypto v1.3.0 // indirect
31+
github.com/andybalholm/brotli v1.0.6 // indirect
3132
github.com/bytedance/sonic v1.11.6 // indirect
3233
github.com/bytedance/sonic/loader v0.1.1 // indirect
3334
github.com/cloudflare/circl v1.6.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
44
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
55
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
66
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
7+
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
8+
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
79
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
810
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
911
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=

internal/logging/request_logger.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
"strings"
1616
"time"
1717

18+
"github.com/andybalholm/brotli"
19+
"github.com/klauspost/compress/zstd"
20+
log "github.com/sirupsen/logrus"
21+
1822
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
1923
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
2024
)
@@ -411,6 +415,10 @@ func (l *FileRequestLogger) decompressResponse(responseHeaders map[string][]stri
411415
return l.decompressGzip(response)
412416
case "deflate":
413417
return l.decompressDeflate(response)
418+
case "br":
419+
return l.decompressBrotli(response)
420+
case "zstd":
421+
return l.decompressZstd(response)
414422
default:
415423
// No compression or unsupported compression
416424
return response, nil
@@ -431,7 +439,9 @@ func (l *FileRequestLogger) decompressGzip(data []byte) ([]byte, error) {
431439
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
432440
}
433441
defer func() {
434-
_ = reader.Close()
442+
if errClose := reader.Close(); errClose != nil {
443+
log.WithError(errClose).Warn("failed to close gzip reader in request logger")
444+
}
435445
}()
436446

437447
decompressed, err := io.ReadAll(reader)
@@ -453,7 +463,9 @@ func (l *FileRequestLogger) decompressGzip(data []byte) ([]byte, error) {
453463
func (l *FileRequestLogger) decompressDeflate(data []byte) ([]byte, error) {
454464
reader := flate.NewReader(bytes.NewReader(data))
455465
defer func() {
456-
_ = reader.Close()
466+
if errClose := reader.Close(); errClose != nil {
467+
log.WithError(errClose).Warn("failed to close deflate reader in request logger")
468+
}
457469
}()
458470

459471
decompressed, err := io.ReadAll(reader)
@@ -464,6 +476,48 @@ func (l *FileRequestLogger) decompressDeflate(data []byte) ([]byte, error) {
464476
return decompressed, nil
465477
}
466478

479+
// decompressBrotli decompresses brotli-encoded data.
480+
//
481+
// Parameters:
482+
// - data: The brotli-encoded data to decompress
483+
//
484+
// Returns:
485+
// - []byte: The decompressed data
486+
// - error: An error if decompression fails, nil otherwise
487+
func (l *FileRequestLogger) decompressBrotli(data []byte) ([]byte, error) {
488+
reader := brotli.NewReader(bytes.NewReader(data))
489+
490+
decompressed, err := io.ReadAll(reader)
491+
if err != nil {
492+
return nil, fmt.Errorf("failed to decompress brotli data: %w", err)
493+
}
494+
495+
return decompressed, nil
496+
}
497+
498+
// decompressZstd decompresses zstd-encoded data.
499+
//
500+
// Parameters:
501+
// - data: The zstd-encoded data to decompress
502+
//
503+
// Returns:
504+
// - []byte: The decompressed data
505+
// - error: An error if decompression fails, nil otherwise
506+
func (l *FileRequestLogger) decompressZstd(data []byte) ([]byte, error) {
507+
decoder, err := zstd.NewReader(bytes.NewReader(data))
508+
if err != nil {
509+
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
510+
}
511+
defer decoder.Close()
512+
513+
decompressed, err := io.ReadAll(decoder)
514+
if err != nil {
515+
return nil, fmt.Errorf("failed to decompress zstd data: %w", err)
516+
}
517+
518+
return decompressed, nil
519+
}
520+
467521
// formatRequestInfo creates the request information section of the log.
468522
//
469523
// Parameters:

internal/runtime/executor/claude_executor.go

Lines changed: 119 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package executor
33
import (
44
"bufio"
55
"bytes"
6+
"compress/flate"
7+
"compress/gzip"
68
"context"
79
"fmt"
810
"io"
911
"net/http"
1012
"strings"
1113
"time"
1214

15+
"github.com/andybalholm/brotli"
1316
"github.com/klauspost/compress/zstd"
1417
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
1518
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
@@ -89,31 +92,31 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
8992
recordAPIResponseError(ctx, e.cfg, err)
9093
return resp, err
9194
}
92-
defer func() {
93-
if errClose := httpResp.Body.Close(); errClose != nil {
94-
log.Errorf("response body close error: %v", errClose)
95-
}
96-
}()
9795
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
9896
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
9997
b, _ := io.ReadAll(httpResp.Body)
10098
appendAPIResponseChunk(ctx, e.cfg, b)
10199
log.Debugf("request error, error status: %d, error body: %s", httpResp.StatusCode, string(b))
102100
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
101+
if errClose := httpResp.Body.Close(); errClose != nil {
102+
log.Errorf("response body close error: %v", errClose)
103+
}
103104
return resp, err
104105
}
105-
reader := io.Reader(httpResp.Body)
106-
var decoder *zstd.Decoder
107-
if hasZSTDEcoding(httpResp.Header.Get("Content-Encoding")) {
108-
decoder, err = zstd.NewReader(httpResp.Body)
109-
if err != nil {
110-
recordAPIResponseError(ctx, e.cfg, err)
111-
return resp, fmt.Errorf("failed to initialize zstd decoder: %w", err)
106+
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
107+
if err != nil {
108+
recordAPIResponseError(ctx, e.cfg, err)
109+
if errClose := httpResp.Body.Close(); errClose != nil {
110+
log.Errorf("response body close error: %v", errClose)
112111
}
113-
reader = decoder
114-
defer decoder.Close()
112+
return resp, err
115113
}
116-
data, err := io.ReadAll(reader)
114+
defer func() {
115+
if errClose := decodedBody.Close(); errClose != nil {
116+
log.Errorf("response body close error: %v", errClose)
117+
}
118+
}()
119+
data, err := io.ReadAll(decodedBody)
117120
if err != nil {
118121
recordAPIResponseError(ctx, e.cfg, err)
119122
return resp, err
@@ -192,19 +195,27 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
192195
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
193196
return nil, err
194197
}
198+
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
199+
if err != nil {
200+
recordAPIResponseError(ctx, e.cfg, err)
201+
if errClose := httpResp.Body.Close(); errClose != nil {
202+
log.Errorf("response body close error: %v", errClose)
203+
}
204+
return nil, err
205+
}
195206
out := make(chan cliproxyexecutor.StreamChunk)
196207
stream = out
197208
go func() {
198209
defer close(out)
199210
defer func() {
200-
if errClose := httpResp.Body.Close(); errClose != nil {
211+
if errClose := decodedBody.Close(); errClose != nil {
201212
log.Errorf("response body close error: %v", errClose)
202213
}
203214
}()
204215

205216
// If from == to (Claude → Claude), directly forward the SSE stream without translation
206217
if from == to {
207-
scanner := bufio.NewScanner(httpResp.Body)
218+
scanner := bufio.NewScanner(decodedBody)
208219
buf := make([]byte, 20_971_520)
209220
scanner.Buffer(buf, 20_971_520)
210221
for scanner.Scan() {
@@ -228,7 +239,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
228239
}
229240

230241
// For other formats, use translation
231-
scanner := bufio.NewScanner(httpResp.Body)
242+
scanner := bufio.NewScanner(decodedBody)
232243
buf := make([]byte, 20_971_520)
233244
scanner.Buffer(buf, 20_971_520)
234245
var param any
@@ -304,29 +315,29 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
304315
recordAPIResponseError(ctx, e.cfg, err)
305316
return cliproxyexecutor.Response{}, err
306317
}
307-
defer func() {
308-
if errClose := resp.Body.Close(); errClose != nil {
309-
log.Errorf("response body close error: %v", errClose)
310-
}
311-
}()
312318
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
313319
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
314320
b, _ := io.ReadAll(resp.Body)
315321
appendAPIResponseChunk(ctx, e.cfg, b)
322+
if errClose := resp.Body.Close(); errClose != nil {
323+
log.Errorf("response body close error: %v", errClose)
324+
}
316325
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
317326
}
318-
reader := io.Reader(resp.Body)
319-
var decoder *zstd.Decoder
320-
if hasZSTDEcoding(resp.Header.Get("Content-Encoding")) {
321-
decoder, err = zstd.NewReader(resp.Body)
322-
if err != nil {
323-
recordAPIResponseError(ctx, e.cfg, err)
324-
return cliproxyexecutor.Response{}, fmt.Errorf("failed to initialize zstd decoder: %w", err)
327+
decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
328+
if err != nil {
329+
recordAPIResponseError(ctx, e.cfg, err)
330+
if errClose := resp.Body.Close(); errClose != nil {
331+
log.Errorf("response body close error: %v", errClose)
325332
}
326-
reader = decoder
327-
defer decoder.Close()
333+
return cliproxyexecutor.Response{}, err
328334
}
329-
data, err := io.ReadAll(reader)
335+
defer func() {
336+
if errClose := decodedBody.Close(); errClose != nil {
337+
log.Errorf("response body close error: %v", errClose)
338+
}
339+
}()
340+
data, err := io.ReadAll(decodedBody)
330341
if err != nil {
331342
recordAPIResponseError(ctx, e.cfg, err)
332343
return cliproxyexecutor.Response{}, err
@@ -419,7 +430,7 @@ func (e *ClaudeExecutor) resolveClaudeConfig(auth *cliproxyauth.Auth) *config.Cl
419430
continue
420431
}
421432
if attrKey != "" && strings.EqualFold(cfgKey, attrKey) {
422-
if attrBase == "" || cfgBase == "" || strings.EqualFold(cfgBase, attrBase) {
433+
if cfgBase == "" || strings.EqualFold(cfgBase, attrBase) {
423434
return entry
424435
}
425436
}
@@ -438,17 +449,84 @@ func (e *ClaudeExecutor) resolveClaudeConfig(auth *cliproxyauth.Auth) *config.Cl
438449
return nil
439450
}
440451

441-
func hasZSTDEcoding(contentEncoding string) bool {
452+
type compositeReadCloser struct {
453+
io.Reader
454+
closers []func() error
455+
}
456+
457+
func (c *compositeReadCloser) Close() error {
458+
var firstErr error
459+
for i := range c.closers {
460+
if c.closers[i] == nil {
461+
continue
462+
}
463+
if err := c.closers[i](); err != nil && firstErr == nil {
464+
firstErr = err
465+
}
466+
}
467+
return firstErr
468+
}
469+
470+
func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) {
471+
if body == nil {
472+
return nil, fmt.Errorf("response body is nil")
473+
}
442474
if contentEncoding == "" {
443-
return false
475+
return body, nil
444476
}
445-
parts := strings.Split(contentEncoding, ",")
446-
for i := range parts {
447-
if strings.EqualFold(strings.TrimSpace(parts[i]), "zstd") {
448-
return true
477+
encodings := strings.Split(contentEncoding, ",")
478+
for _, raw := range encodings {
479+
encoding := strings.TrimSpace(strings.ToLower(raw))
480+
switch encoding {
481+
case "", "identity":
482+
continue
483+
case "gzip":
484+
gzipReader, err := gzip.NewReader(body)
485+
if err != nil {
486+
_ = body.Close()
487+
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
488+
}
489+
return &compositeReadCloser{
490+
Reader: gzipReader,
491+
closers: []func() error{
492+
gzipReader.Close,
493+
func() error { return body.Close() },
494+
},
495+
}, nil
496+
case "deflate":
497+
deflateReader := flate.NewReader(body)
498+
return &compositeReadCloser{
499+
Reader: deflateReader,
500+
closers: []func() error{
501+
deflateReader.Close,
502+
func() error { return body.Close() },
503+
},
504+
}, nil
505+
case "br":
506+
return &compositeReadCloser{
507+
Reader: brotli.NewReader(body),
508+
closers: []func() error{
509+
func() error { return body.Close() },
510+
},
511+
}, nil
512+
case "zstd":
513+
decoder, err := zstd.NewReader(body)
514+
if err != nil {
515+
_ = body.Close()
516+
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
517+
}
518+
return &compositeReadCloser{
519+
Reader: decoder,
520+
closers: []func() error{
521+
func() error { decoder.Close(); return nil },
522+
func() error { return body.Close() },
523+
},
524+
}, nil
525+
default:
526+
continue
449527
}
450528
}
451-
return false
529+
return body, nil
452530
}
453531

454532
func applyClaudeHeaders(r *http.Request, apiKey string, stream bool) {

0 commit comments

Comments
 (0)