diff --git a/circuitbreaker/circuitbreaker.go b/circuitbreaker/circuitbreaker.go index 4d5b4dc..25fdb1e 100644 --- a/circuitbreaker/circuitbreaker.go +++ b/circuitbreaker/circuitbreaker.go @@ -1,8 +1,10 @@ package circuitbreaker import ( + "bufio" "errors" "fmt" + "net" "net/http" "sync" "time" @@ -101,7 +103,8 @@ func (p *ProviderCircuitBreakers) Get(endpoint, model string) *gobreaker.Circuit } // statusCapturingWriter wraps http.ResponseWriter to capture the status code. -// It also implements http.Flusher to support streaming responses. +// It implements http.Flusher to support streaming and http.Hijacker to +// satisfy the FullResponseWriter lint rule. type statusCapturingWriter struct { http.ResponseWriter statusCode int @@ -130,6 +133,14 @@ func (w *statusCapturingWriter) Flush() { } } +func (w *statusCapturingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + h, ok := w.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, xerrors.New("upstream ResponseWriter does not support hijacking") + } + return h.Hijack() +} + // Unwrap returns the underlying ResponseWriter for interface checks. func (w *statusCapturingWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter diff --git a/intercept/chatcompletions/paramswrap.go b/intercept/chatcompletions/paramswrap.go index ab6dd52..7c87d8f 100644 --- a/intercept/chatcompletions/paramswrap.go +++ b/intercept/chatcompletions/paramswrap.go @@ -52,7 +52,7 @@ func (c *ChatCompletionNewParamsWrapper) lastUserPrompt() (*string, error) { // We only care if the last message was issued by a user. msg := c.Messages[len(c.Messages)-1] if msg.OfUser == nil { - return nil, nil + return nil, nil //nolint:nilnil // no user prompt found is not an error } if msg.OfUser.Content.OfString.String() != "" { @@ -69,5 +69,5 @@ func (c *ChatCompletionNewParamsWrapper) lastUserPrompt() (*string, error) { } } - return nil, nil + return nil, nil //nolint:nilnil // no text content found is not an error } diff --git a/intercept/messages/base_test.go b/intercept/messages/base_test.go index ff0a20d..eb073ab 100644 --- a/intercept/messages/base_test.go +++ b/intercept/messages/base_test.go @@ -812,7 +812,7 @@ func (m *mockServerProxier) GetTool(id string) *mcp.Tool { } func (*mockServerProxier) CallTool(context.Context, string, any) (*mcpgo.CallToolResult, error) { - return nil, nil + return nil, nil //nolint:nilnil // mock: no-op implementation } func TestFilterBedrockBetaFlags(t *testing.T) { diff --git a/intercept/messages/streaming.go b/intercept/messages/streaming.go index 5ee7b96..cc74828 100644 --- a/intercept/messages/streaming.go +++ b/intercept/messages/streaming.go @@ -515,11 +515,11 @@ newStream: } shutdownCtx, shutdownCancel := context.WithTimeout(ctx, time.Second*30) - defer shutdownCancel() // Give the events stream 30 seconds (TODO: configurable) to gracefully shutdown. if err := events.Shutdown(shutdownCtx); err != nil { logger.Warn(ctx, "event stream shutdown", slog.Error(err)) } + shutdownCancel() // Cancel the stream context, we're now done. if interceptionErr != nil { diff --git a/intercept/responses/base.go b/intercept/responses/base.go index 38cb8f9..cbd63df 100644 --- a/intercept/responses/base.go +++ b/intercept/responses/base.go @@ -127,7 +127,13 @@ func (i *responsesInterceptionBase) validateRequest(ctx context.Context, w http. // sendCustomErr sends custom responses.Error error to the client // it should only be called before any data is sent back to the client func (i *responsesInterceptionBase) sendCustomErr(ctx context.Context, w http.ResponseWriter, code int, err error) { - respErr := responses.Error{ + // Same JSON shape as responses.Error but using a plain struct because + // responses.Error embeds *http.Request whose GetBody func field + // is not JSON-marshalable (SA1026). + respErr := struct { + Code string `json:"code"` + Message string `json:"message"` + }{ Code: strconv.Itoa(code), Message: err.Error(), } diff --git a/internal/testutil/mockprovider.go b/internal/testutil/mockprovider.go index c8982fe..3e1db79 100644 --- a/internal/testutil/mockprovider.go +++ b/internal/testutil/mockprovider.go @@ -32,5 +32,5 @@ func (m *MockProvider) CreateInterceptor(w http.ResponseWriter, r *http.Request, if m.InterceptorFunc != nil { return m.InterceptorFunc(w, r, tracer) } - return nil, nil + return nil, nil //nolint:nilnil // mock: no interceptor configured is not an error }