Handle compressed error decode failures safely
This commit is contained in:
@@ -185,14 +185,25 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API)
|
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API).
|
||||||
errBody := httpResp.Body
|
errBody := httpResp.Body
|
||||||
if ce := httpResp.Header.Get("Content-Encoding"); ce != "" {
|
if ce := httpResp.Header.Get("Content-Encoding"); ce != "" {
|
||||||
if decoded, decErr := decodeResponseBody(httpResp.Body, ce); decErr == nil {
|
var decErr error
|
||||||
errBody = decoded
|
errBody, decErr = decodeResponseBody(httpResp.Body, ce)
|
||||||
|
if decErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
|
msg := fmt.Sprintf("failed to decode error response body (encoding=%s): %v", ce, decErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
return resp, statusErr{code: httpResp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b, _ := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
|
if readErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
b = []byte(msg)
|
||||||
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
@@ -339,14 +350,25 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API)
|
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API).
|
||||||
errBody := httpResp.Body
|
errBody := httpResp.Body
|
||||||
if ce := httpResp.Header.Get("Content-Encoding"); ce != "" {
|
if ce := httpResp.Header.Get("Content-Encoding"); ce != "" {
|
||||||
if decoded, decErr := decodeResponseBody(httpResp.Body, ce); decErr == nil {
|
var decErr error
|
||||||
errBody = decoded
|
errBody, decErr = decodeResponseBody(httpResp.Body, ce)
|
||||||
|
if decErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
|
msg := fmt.Sprintf("failed to decode error response body (encoding=%s): %v", ce, decErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
return nil, statusErr{code: httpResp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b, _ := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
|
if readErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
b = []byte(msg)
|
||||||
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := errBody.Close(); errClose != nil {
|
if errClose := errBody.Close(); errClose != nil {
|
||||||
@@ -497,14 +519,25 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API)
|
// Decompress error responses (e.g. gzip-compressed 400 errors from Anthropic API).
|
||||||
errBody := io.ReadCloser(resp.Body)
|
errBody := resp.Body
|
||||||
if ce := resp.Header.Get("Content-Encoding"); ce != "" {
|
if ce := resp.Header.Get("Content-Encoding"); ce != "" {
|
||||||
if decoded, decErr := decodeResponseBody(resp.Body, ce); decErr == nil {
|
var decErr error
|
||||||
errBody = decoded
|
errBody, decErr = decodeResponseBody(resp.Body, ce)
|
||||||
|
if decErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
|
msg := fmt.Sprintf("failed to decode error response body (encoding=%s): %v", ce, decErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b, _ := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
|
if readErr != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
|
logWithRequestID(ctx).Warn(msg)
|
||||||
|
b = []byte(msg)
|
||||||
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
if errClose := errBody.Close(); errClose != nil {
|
if errClose := errBody.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
@@ -519,3 +520,66 @@ func hasTTLOrderingViolation(payload []byte) bool {
|
|||||||
|
|
||||||
return violates
|
return violates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClaudeExecutor_Execute_InvalidGzipErrorBodyReturnsDecodeMessage(t *testing.T) {
|
||||||
|
testClaudeExecutorInvalidCompressedErrorBody(t, func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error {
|
||||||
|
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||||
|
Model: "claude-3-5-sonnet-20241022",
|
||||||
|
Payload: payload,
|
||||||
|
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaudeExecutor_ExecuteStream_InvalidGzipErrorBodyReturnsDecodeMessage(t *testing.T) {
|
||||||
|
testClaudeExecutorInvalidCompressedErrorBody(t, func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error {
|
||||||
|
_, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
|
||||||
|
Model: "claude-3-5-sonnet-20241022",
|
||||||
|
Payload: payload,
|
||||||
|
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaudeExecutor_CountTokens_InvalidGzipErrorBodyReturnsDecodeMessage(t *testing.T) {
|
||||||
|
testClaudeExecutorInvalidCompressedErrorBody(t, func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error {
|
||||||
|
_, err := executor.CountTokens(context.Background(), auth, cliproxyexecutor.Request{
|
||||||
|
Model: "claude-3-5-sonnet-20241022",
|
||||||
|
Payload: payload,
|
||||||
|
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClaudeExecutorInvalidCompressedErrorBody(
|
||||||
|
t *testing.T,
|
||||||
|
invoke func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte("not-a-valid-gzip-stream"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
executor := NewClaudeExecutor(&config.Config{})
|
||||||
|
auth := &cliproxyauth.Auth{Attributes: map[string]string{
|
||||||
|
"api_key": "key-123",
|
||||||
|
"base_url": server.URL,
|
||||||
|
}}
|
||||||
|
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
|
||||||
|
|
||||||
|
err := invoke(executor, auth, payload)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "failed to decode error response body") {
|
||||||
|
t.Fatalf("expected decode failure message, got: %v", err)
|
||||||
|
}
|
||||||
|
if statusProvider, ok := err.(interface{ StatusCode() int }); !ok || statusProvider.StatusCode() != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected status code 400, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user