mirror of
https://github.com/karust/openserp.git
synced 2026-08-24 17:00:06 +08:00
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.
This commit is contained in:
71
core/server_panic_test.go
Normal file
71
core/server_panic_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user