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 {