From 38822bbfaab425437d94d3957207f5ea625bb271 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Fri, 2 Oct 2026 16:46:22 +0800 Subject: [PATCH] Go SDK: reject a task function whose last result type is not error Signed-off-by: PoAn Yang --- go-sdk/airflow/dag_test.go | 5 +++++ go-sdk/airflow/task_handler_test.go | 9 +++++++++ go-sdk/internal/bundle/task.go | 15 +++++++++++++-- go-sdk/internal/bundle/task_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/go-sdk/airflow/dag_test.go b/go-sdk/airflow/dag_test.go index ada37091ee3dc..e9b56ca7fd07a 100644 --- a/go-sdk/airflow/dag_test.go +++ b/go-sdk/airflow/dag_test.go @@ -316,6 +316,11 @@ func TestTaskPanicsOnBadFunction(t *testing.T) { fn: func(context.Context) error { return nil }, want: "parameter 0 is context.Context, but the first parameter must be airflow.Context", }, + { + name: "error result of a concrete type", + fn: func(Context) (int, *taskError) { return 0, nil }, + want: "must declare its last result as error, not *airflow.taskError", + }, { // The name of a method expression keeps a dot, as a function literal's name does. // This row checks that the signature check rejects the method expression for its diff --git a/go-sdk/airflow/task_handler_test.go b/go-sdk/airflow/task_handler_test.go index 8dfa7707d6372..157494bc63fc0 100644 --- a/go-sdk/airflow/task_handler_test.go +++ b/go-sdk/airflow/task_handler_test.go @@ -85,6 +85,10 @@ func panicMessage(t *testing.T, f func()) (msg string) { return "" } +type taskError struct{} + +func (*taskError) Error() string { return "task error" } + func TestTaskHandlerPanicsOnBadHandler(t *testing.T) { var unassigned func(Context) error @@ -115,6 +119,11 @@ func TestTaskHandlerPanicsOnBadHandler(t *testing.T) { fn: func(Context) int { return 0 }, want: "last return value to return error but found int", }, + { + name: "error result of a concrete type", + fn: func(Context) (int, *taskError) { return 0, nil }, + want: "must declare its last result as error, not *airflow.taskError", + }, { name: "parameter that cannot hold an argument", fn: func(Context, chan int) error { return nil }, diff --git a/go-sdk/internal/bundle/task.go b/go-sdk/internal/bundle/task.go index d2353369be8e3..8a1743eabf087 100644 --- a/go-sdk/internal/bundle/task.go +++ b/go-sdk/internal/bundle/task.go @@ -195,11 +195,22 @@ func (f *taskFunction) validateFn( fnType.Out(0).Kind(), ) } - if !isError(fnType.Out(fnType.NumOut() - 1)) { + last := fnType.Out(fnType.NumOut() - 1) + if !isError(last) { return fmt.Errorf( "expected task function %s last return value to return error but found %v", f.fullName, - fnType.Out(fnType.NumOut()-1).Kind(), + last.Kind(), + ) + } + // The last result must be error itself, not just a type that implements error. A nil *MyErr, + // for example, becomes a non-nil error when call converts it. The task would then fail even + // though the function returned nil. + if last != errorType { + return fmt.Errorf( + "task function %s must declare its last result as error, not %s", + f.fullName, + last, ) } diff --git a/go-sdk/internal/bundle/task_test.go b/go-sdk/internal/bundle/task_test.go index d3633e9449c71..d1d302ab0ec07 100644 --- a/go-sdk/internal/bundle/task_test.go +++ b/go-sdk/internal/bundle/task_test.go @@ -49,6 +49,19 @@ func TestTaskSuite(t *testing.T) { suite.Run(t, &TaskSuite{}) } +type taskError struct{} + +func (*taskError) Error() string { return "task error" } + +type errno int + +func (errno) Error() string { return "errno" } + +type codedError interface { + error + Code() int +} + func (s *TaskSuite) TestReturnValidation() { cases := map[string]struct { fn any @@ -66,6 +79,18 @@ func (s *TaskSuite) TestReturnValidation() { func(contexttest.Context) (c chan int) { return }, `func\d+ last return value to return error but found chan`, }, + "pointer-error-ret": { + func(contexttest.Context) (int, *taskError) { return 0, nil }, + `func\d+ must declare its last result as error, not \*bundle\.taskError$`, + }, + "value-error-ret": { + func(contexttest.Context) errno { return 0 }, + `func\d+ must declare its last result as error, not bundle\.errno$`, + }, + "interface-error-ret": { + func(contexttest.Context) (int, codedError) { return 0, nil }, + `func\d+ must declare its last result as error, not bundle\.codedError$`, + }, } for name, tt := range cases {