fix(cliproxy): centralize client error status mapping and apply context cancellation/deadline HTTP codes

Closes: #4601
This commit is contained in:
Luis Pater
2026-08-08 04:53:34 +08:00
parent 37609fa179
commit 4b3cc55cdc
15 changed files with 297 additions and 59 deletions

View File

@@ -1,8 +1,11 @@
package clienterror
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"testing"
)
@@ -14,6 +17,94 @@ type statusError struct {
func (e statusError) Error() string { return e.body }
func (e statusError) StatusCode() int { return e.status }
func TestHTTPStatusFromError(t *testing.T) {
tests := []struct {
name string
err error
want int
}{
{name: "nil", err: nil, want: 0},
{name: "plain error", err: errors.New("boom"), want: 0},
{name: "context canceled", err: context.Canceled, want: StatusClientClosedRequest},
{name: "context deadline exceeded", err: context.DeadlineExceeded, want: http.StatusGatewayTimeout},
{
name: "url error wraps canceled",
err: &url.Error{Op: "Post", URL: "https://example.com", Err: context.Canceled},
want: StatusClientClosedRequest,
},
{
name: "url error wraps deadline",
err: &url.Error{Op: "Post", URL: "https://example.com", Err: context.DeadlineExceeded},
want: http.StatusGatewayTimeout,
},
{
name: "fmt wrap canceled",
err: fmt.Errorf("upstream: %w", context.Canceled),
want: StatusClientClosedRequest,
},
{
name: "explicit status code wins",
err: statusError{status: http.StatusTooManyRequests, body: "rate limited"},
want: http.StatusTooManyRequests,
},
{
name: "explicit status wins over canceled unwrap",
err: statusAndUnwrapError{
status: http.StatusTooManyRequests,
body: "rate limited",
cause: context.Canceled,
},
want: http.StatusTooManyRequests,
},
{
name: "zero status code falls through to canceled unwrap",
err: statusAndUnwrapError{
status: 0,
body: "canceled",
cause: context.Canceled,
},
want: StatusClientClosedRequest,
},
{
name: "zero status code without unwrap stays unknown",
err: statusError{status: 0, body: context.Canceled.Error()},
want: 0,
},
{
name: "wrapped status code via errors.As",
err: fmt.Errorf("execute failed: %w", statusError{status: http.StatusUnauthorized, body: "unauthorized"}),
want: http.StatusUnauthorized,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := HTTPStatusFromError(tc.err); got != tc.want {
t.Fatalf("HTTPStatusFromError() = %d, want %d", got, tc.want)
}
})
}
if got := HTTPStatusFromErrorOr(errors.New("boom"), http.StatusBadGateway); got != http.StatusBadGateway {
t.Fatalf("HTTPStatusFromErrorOr(plain) = %d, want %d", got, http.StatusBadGateway)
}
if got := HTTPStatusFromErrorOr(context.Canceled, http.StatusBadGateway); got != StatusClientClosedRequest {
t.Fatalf("HTTPStatusFromErrorOr(canceled) = %d, want %d", got, StatusClientClosedRequest)
}
}
type statusAndUnwrapError struct {
status int
body string
cause error
}
func (e statusAndUnwrapError) Error() string { return e.body }
func (e statusAndUnwrapError) StatusCode() int {
return e.status
}
func (e statusAndUnwrapError) Unwrap() error { return e.cause }
func TestIsRequestFaultStructuredIdentifiers(t *testing.T) {
for _, code := range []string{
"cyber_policy",