Files
warkanum 1953a4f4f9 feat(server): add passthrough proxy for OpenAI-compatible endpoints
* implement proxy handler for various OpenAI API routes
* add error handling for request body and response streaming
* introduce new error response format for API compatibility
* add tests for recover middleware to handle panics gracefully
2026-08-01 20:15:59 +02:00

89 lines
2.9 KiB
Go

package server
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/uptrace/bunrouter"
"go.uber.org/zap"
)
// TestRecoverMiddlewarePanicBeforeResponse verifies a panic in a handler that
// hasn't written anything yet is converted into a 500 JSON error instead of
// crashing the request goroutine (which would otherwise surface to the client
// as a connection reset).
func TestRecoverMiddlewarePanicBeforeResponse(t *testing.T) {
mw := recoverMiddleware(zap.NewNop())
handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
panic("boom")
})
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
err := handler(w, bunrouter.Request{Request: req})
if err != nil {
t.Fatalf("expected recoverMiddleware to swallow the panic and return nil, got %v", err)
}
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected status 500, got %d", w.Code)
}
var body apiErrorEnvelope
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("expected a valid JSON error body, got decode error: %v (body=%q)", err, w.Body.String())
}
if body.Error.Type != "internal_error" {
t.Fatalf("expected error type internal_error, got %q", body.Error.Type)
}
}
// TestRecoverMiddlewarePanicAfterResponse verifies a panic that happens after
// the response has already been committed (e.g. mid-stream) doesn't attempt a
// second WriteHeader — it just surfaces as an error for the logger.
func TestRecoverMiddlewarePanicAfterResponse(t *testing.T) {
mw := recoverMiddleware(zap.NewNop())
handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("partial"))
panic("boom mid-stream")
})
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/chat/completions", nil)
w := httptest.NewRecorder()
err := handler(w, bunrouter.Request{Request: req})
if err == nil {
t.Fatal("expected an error to be returned once the response was already committed")
}
if w.Code != http.StatusOK {
t.Fatalf("expected the original 200 to stand, got %d", w.Code)
}
if w.Body.String() != "partial" {
t.Fatalf("expected body to be left as-is, got %q", w.Body.String())
}
}
// TestRecoverMiddlewareNoPanic verifies the happy path is unaffected.
func TestRecoverMiddlewareNoPanic(t *testing.T) {
mw := recoverMiddleware(zap.NewNop())
handler := mw(func(w http.ResponseWriter, req bunrouter.Request) error {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
return nil
})
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
if err := handler(w, bunrouter.Request{Request: req}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if w.Code != http.StatusOK || w.Body.String() != "ok" {
t.Fatalf("unexpected response: status=%d body=%q", w.Code, w.Body.String())
}
}