From 668b31784ce0f8ad7d21027211f18875d1d9259a Mon Sep 17 00:00:00 2001 From: devmanishofficial Date: Mon, 29 Dec 2025 22:41:35 +0530 Subject: [PATCH 1/4] fix(go): implement Unwrap for GenkitError to support error chains Signed-off-by: devmanishofficial --- go/core/error.go | 36 ++++++++++++++++++++++++++---------- go/core/error_test.go | 17 +++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index ba70d7f76e..1861ef956b 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -37,11 +37,12 @@ type ReflectionError struct { // GenkitError is the base error type for Genkit errors. type GenkitError struct { - Message string `json:"message"` // Exclude from default JSON if embedded elsewhere - Status StatusName `json:"status"` - HTTPCode int `json:"-"` // Exclude from default JSON - Details map[string]any `json:"details"` // Use map for arbitrary details - Source *string `json:"source,omitempty"` // Pointer for optional + Message string `json:"message"` // Exclude from default JSON if embedded elsewhere + Status StatusName `json:"status"` + HTTPCode int `json:"-"` // Exclude from default JSON + Details map[string]any `json:"details"` // Use map for arbitrary details + Source *string `json:"source,omitempty"` // Pointer for optional + originalError error // The wrapped error, if any } // UserFacingError is the base error type for user facing errors. @@ -78,6 +79,13 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { Message: fmt.Sprintf(msg, args...), } + // scan args for the last error to wrap it + for _, arg := range args { + if err, ok := arg.(error); ok { + ge.originalError = err + } + } + errStack := string(debug.Stack()) if errStack != "" { ge.Details = make(map[string]any) @@ -91,14 +99,22 @@ func (e *GenkitError) Error() string { return e.Message } +// Unwrap implements the standard error unwrapping interface. +// This allows errors.Is and errors.As to work with GenkitError. +func (e *GenkitError) Unwrap() error { + return e.originalError +} + // ToReflectionError returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToReflectionError() ReflectionError { errDetails := &ReflectionErrorDetails{} - if stackVal, ok := e.Details["stack"].(string); ok { - errDetails.Stack = &stackVal - } - if traceVal, ok := e.Details["traceId"].(string); ok { - errDetails.TraceID = &traceVal + if e.Details != nil { + if stackVal, ok := e.Details["stack"].(string); ok { + errDetails.Stack = &stackVal + } + if traceVal, ok := e.Details["traceId"].(string); ok { + errDetails.TraceID = &traceVal + } } return ReflectionError{ Details: errDetails, diff --git a/go/core/error_test.go b/go/core/error_test.go index 60ff503bdc..4a4fb0833d 100644 --- a/go/core/error_test.go +++ b/go/core/error_test.go @@ -163,6 +163,23 @@ func TestGenkitErrorToReflectionError(t *testing.T) { }) } +func TestGenkitErrorUnwrap(t *testing.T) { + original := errors.New("original failure") + + // Use INTERNAL instead of StatusInternal + gErr := NewError(INTERNAL, "something happened: %v", original) + + // Verify errors.Is works (this is the most important check) + if !errors.Is(gErr, original) { + t.Errorf("expected errors.Is to return true, but got false") + } + + // Verify Unwrap works directly + if gErr.Unwrap() != original { + t.Errorf("Unwrap() returned wrong error") + } +} + func TestToReflectionError(t *testing.T) { t.Run("handles GenkitError directly", func(t *testing.T) { ge := NewError(INVALID_ARGUMENT, "bad input") From 0b76ae6775b820fd96a3b1ea2bacfd5fde959ed9 Mon Sep 17 00:00:00 2001 From: devmanishofficial Date: Tue, 30 Dec 2025 00:15:30 +0530 Subject: [PATCH 2/4] fix: remove accidental telemetry changes and optimize error scan --- go/core/error.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 1861ef956b..12671dd6c5 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -71,7 +71,6 @@ func (e *UserFacingError) Error() string { // NewError creates a new GenkitError with a stack trace. func NewError(status StatusName, message string, args ...any) *GenkitError { - // Prevents a compile-time warning about non-constant message. msg := message ge := &GenkitError{ @@ -79,10 +78,11 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { Message: fmt.Sprintf(msg, args...), } - // scan args for the last error to wrap it - for _, arg := range args { - if err, ok := arg.(error); ok { + // scan args for the last error to wrap it (Iterate backwards) + for i := len(args) - 1; i >= 0; i-- { + if err, ok := args[i].(error); ok { ge.originalError = err + break } } From 110e9c3bd7cfb4a8106ff4c094302c6b3fb4b979 Mon Sep 17 00:00:00 2001 From: Zereker Date: Mon, 9 Feb 2026 20:54:12 +0800 Subject: [PATCH 3/4] test(go): add comprehensive error chain subtests for GenkitError Extend TestGenkitErrorUnwrap (introduced by @DEVMANISHOFFL in #4018) with additional t.Run subtests covering errors.As, no-args, multiple errors, and non-error args edge cases. --- go/core/error_test.go | 70 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/go/core/error_test.go b/go/core/error_test.go index 4a4fb0833d..cffee3de7d 100644 --- a/go/core/error_test.go +++ b/go/core/error_test.go @@ -163,21 +163,69 @@ func TestGenkitErrorToReflectionError(t *testing.T) { }) } +// testCustomError is a helper type for the errors.As subtest. +type testCustomError struct { + code int +} + +func (e *testCustomError) Error() string { + return fmt.Sprintf("custom error %d", e.code) +} + func TestGenkitErrorUnwrap(t *testing.T) { - original := errors.New("original failure") + t.Run("errors.Is matches original cause", func(t *testing.T) { + original := errors.New("original failure") + gErr := NewError(INTERNAL, "something happened: %v", original) + + if !errors.Is(gErr, original) { + t.Errorf("expected errors.Is to return true, but got false") + } + if gErr.Unwrap() != original { + t.Errorf("Unwrap() returned wrong error") + } + }) + + t.Run("errors.As extracts typed cause", func(t *testing.T) { + cause := &testCustomError{code: 42} + ge := NewError(INTERNAL, "failed: %v", cause) - // Use INTERNAL instead of StatusInternal - gErr := NewError(INTERNAL, "something happened: %v", original) + var target *testCustomError + if !errors.As(ge, &target) { + t.Fatal("errors.As failed to find *testCustomError") + } + if target.code != 42 { + t.Errorf("target.code = %d, want 42", target.code) + } + }) - // Verify errors.Is works (this is the most important check) - if !errors.Is(gErr, original) { - t.Errorf("expected errors.Is to return true, but got false") - } + t.Run("no args returns nil", func(t *testing.T) { + ge := NewError(INTERNAL, "no args error") - // Verify Unwrap works directly - if gErr.Unwrap() != original { - t.Errorf("Unwrap() returned wrong error") - } + if ge.Unwrap() != nil { + t.Errorf("Unwrap() = %v, want nil", ge.Unwrap()) + } + }) + + t.Run("multiple errors preserves the last one", func(t *testing.T) { + first := errors.New("first") + second := errors.New("second") + ge := NewError(INTERNAL, "two errors: %v %v", first, second) + + if ge.Unwrap() != second { + t.Errorf("Unwrap() = %v, want %v (last error)", ge.Unwrap(), second) + } + if !errors.Is(ge, second) { + t.Error("errors.Is(ge, second) = false, want true") + } + }) + + t.Run("non-error args returns nil", func(t *testing.T) { + ge := NewError(INTERNAL, "value is %d and %s", 42, "hello") + + if ge.Unwrap() != nil { + t.Errorf("Unwrap() = %v, want nil", ge.Unwrap()) + } + }) } func TestToReflectionError(t *testing.T) { From 7151e0587e30cc05c2f6fda420618876307490c8 Mon Sep 17 00:00:00 2001 From: Zereker Date: Mon, 9 Feb 2026 21:12:27 +0800 Subject: [PATCH 4/4] refactor(go/core): omit empty details in ToReflectionError JSON output Only allocate ReflectionErrorDetails when stack or traceId is present, leveraging the omitempty tag to produce cleaner JSON output. --- go/core/error.go | 18 ++++++++++++------ go/core/error_test.go | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/go/core/error.go b/go/core/error.go index 12671dd6c5..4482fc21fd 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -107,13 +107,19 @@ func (e *GenkitError) Unwrap() error { // ToReflectionError returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToReflectionError() ReflectionError { - errDetails := &ReflectionErrorDetails{} + var errDetails *ReflectionErrorDetails if e.Details != nil { - if stackVal, ok := e.Details["stack"].(string); ok { - errDetails.Stack = &stackVal - } - if traceVal, ok := e.Details["traceId"].(string); ok { - errDetails.TraceID = &traceVal + stackVal, stackOk := e.Details["stack"].(string) + traceVal, traceOk := e.Details["traceId"].(string) + + if stackOk || traceOk { + errDetails = &ReflectionErrorDetails{} + if stackOk { + errDetails.Stack = &stackVal + } + if traceOk { + errDetails.TraceID = &traceVal + } } } return ReflectionError{ diff --git a/go/core/error_test.go b/go/core/error_test.go index cffee3de7d..a2e26a25a8 100644 --- a/go/core/error_test.go +++ b/go/core/error_test.go @@ -157,8 +157,8 @@ func TestGenkitErrorToReflectionError(t *testing.T) { if re.Message != "success" { t.Errorf("Message = %q, want %q", re.Message, "success") } - if re.Details.Stack != nil { - t.Error("expected nil stack") + if re.Details != nil { + t.Error("expected nil details") } }) }