This is an automated email from the ASF dual-hosted git repository.

henry3260 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 7a8ce83f385 Go SDK: reject a task function whose last result type is 
not error (#74077)
7a8ce83f385 is described below

commit 7a8ce83f3851ec65dd2186c75f90e85c666a7cc0
Author: PoAn Yang <[email protected]>
AuthorDate: Sat Oct 3 11:41:14 2026 +0800

    Go SDK: reject a task function whose last result type is not error (#74077)
    
    Signed-off-by: PoAn Yang <[email protected]>
---
 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 ada37091ee3..e9b56ca7fd0 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 8dfa7707d63..157494bc63f 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 d2353369be8..8a1743eabf0 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 d3633e9449c..d1d302ab0ec 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 {

Reply via email to