Files
multica/server/internal/handler/internal_error_boundary_test.go
e7742b052b MUL-7776: fix(api): stop returning raw error text in 500 responses (#8905)
* fix(api): stop returning raw error text in 500 responses

39 handlers answered a 500 with the message followed by err.Error(),
sending database and store errors (constraint names, SQL states, Redis
errors) to API clients, while the other 720 500 responses in the handler
package send a fixed message. These now send their fixed message too and
log the error with the request's attributes, as the handlers that already
logged it do.

* fix(api): drop raw error text from the task callbacks and skill import 500s

completeTask and failTask sent err.Error() as the whole 500 message, and
the structured skill import put it in the result's Reason on its three
500s. They now send fixed messages and log the error. The task callbacks
keep their 500, so the daemon still retries them: isTransientError reads
the status code, never the body.

* fix(api): close remaining raw error leaks

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-10 12:17:42 +08:00

68 lines
1.8 KiB
Go

package handler
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
// TestInternalServerErrorsDoNotExposeCause keeps raw infrastructure errors on
// the server side. It walks complete call expressions, so multiline payloads
// and error variables with names other than err are covered too.
func TestInternalServerErrorsDoNotExposeCause(t *testing.T) {
t.Parallel()
entries, err := os.ReadDir(".")
if err != nil {
t.Fatalf("read handler package: %v", err)
}
fset := token.NewFileSet()
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
file, err := parser.ParseFile(fset, filepath.Clean(name), nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok || !callUsesInternalServerError(call) {
return true
}
ast.Inspect(call, func(child ast.Node) bool {
causeCall, ok := child.(*ast.CallExpr)
if !ok {
return true
}
selector, ok := causeCall.Fun.(*ast.SelectorExpr)
if ok && selector.Sel.Name == "Error" && len(causeCall.Args) == 0 {
pos := fset.Position(causeCall.Pos())
t.Errorf("%s:%d returns a raw error from an internal-server-error path; send stable public copy and log the cause", name, pos.Line)
}
return true
})
return false
})
}
}
func callUsesInternalServerError(call *ast.CallExpr) bool {
for _, arg := range call.Args {
selector, ok := arg.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "StatusInternalServerError" {
continue
}
pkg, ok := selector.X.(*ast.Ident)
if ok && pkg.Name == "http" {
return true
}
}
return false
}