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()) } }