Files
openserp/core/server_panic_test.go
Rustem Kamalov d20a7c53cd fix: centralize engine panic recovery in resilient layer
A rod panic during SearchImage propagated uncaught into fasthttp and killed the whole process: per-engine recover blocks only covered Search (5 of 6 engines had no recovery on SearchImage), and there was no Fiber
recover middleware.

Regression test: a panicking SearchImage returns 502 engine_internal and the server keeps serving.
2026-06-12 04:24:06 +03:00

72 lines
2.1 KiB
Go

package core
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
// FP-1: panics from engine code must be converted to ErrEngineInternal by the
// central recovery in the resilient layer (invokeEngine) instead of killing
// the process. SearchImage had no per-engine recovery in 5 of 6 engines.
func TestInvokeEngineRecoversPanics(t *testing.T) {
engine := &engineMock{
name: "google",
initialized: true,
searchFn: func(_ context.Context, _ Query) ([]SearchResult, error) {
panic("rod: page crashed")
},
imageFn: func(_ context.Context, _ Query) ([]SearchResult, error) {
panic("rod: object not found")
},
}
for _, isImage := range []bool{false, true} {
results, err := invokeEngine(context.Background(), engine, Query{Text: "golang"}, isImage)
if results != nil {
t.Fatalf("isImage=%v: expected nil results after panic, got %v", isImage, results)
}
if !errors.Is(err, ErrEngineInternal) {
t.Fatalf("isImage=%v: expected ErrEngineInternal, got %v", isImage, err)
}
}
}
func TestPanickingSearchImageReturns502AndServerSurvives(t *testing.T) {
engine := &engineMock{
name: "google",
initialized: true,
imageFn: func(_ context.Context, _ Query) ([]SearchResult, error) {
panic("rod: page crashed")
},
}
opts := DefaultServerOptions()
opts.Resilience.Retry.MaxRetries = 0
srv := NewServerWithOptions("127.0.0.1", 7130, opts, engine)
req := httptest.NewRequest(http.MethodGet, "/google/image?text=golang", nil)
resp, err := srv.app.Test(req, -1)
if err != nil {
t.Fatalf("image request failed: %v", err)
}
if resp.StatusCode != http.StatusBadGateway {
t.Fatalf("expected 502 for panicking SearchImage, got %d", resp.StatusCode)
}
var payload JSONErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
t.Fatalf("decode error response: %v", err)
}
if payload.Error != "engine_internal" {
t.Fatalf("expected error=engine_internal, got %q", payload.Error)
}
second := request(t, srv, "/google/search?text=golang")
if second.StatusCode != http.StatusOK {
t.Fatalf("expected server to keep serving after panic, got %d", second.StatusCode)
}
}