apimachinery: clarify ErrorHandler and provide helper for formatting

It turned out that some downstream consumers converted their code after the API
change in 1.32 (5a130d2b71) by simply taking the
error from the parameters, ignoring the rest. Not only is this capturing the
problem incompletely, it will also crash for those
HandleErrorWithContext/Logger calls which pass a nil error.

Additional documentation might help a bit. The new ErrorToString helper can
be used with or without errors.New to represent the full problem the way
downstream consumers were traditionally expecting it.
This commit is contained in:
Patrick Ohly
2025-11-24 10:53:24 +01:00
parent f8b277b2b8
commit 20f95d2f2b
2 changed files with 68 additions and 0 deletions

View File

@@ -17,14 +17,17 @@ limitations under the License.
package runtime
import (
"bytes"
"context"
"fmt"
"net/http"
"runtime"
"strings"
"sync"
"time"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
)
var (
@@ -154,8 +157,37 @@ var ErrorHandlers = []ErrorHandler{
backoffError(1 * time.Millisecond),
}
// ErrorHandler is called indirectly through [HandleError], [HandleErrorWithContext] or [HandleErrorWithLogger].
// It is passed the same parameters that a structured logging backend needs to log a problem.
// It follows the semantic described for [HandleErrorWithContext] and [logr.Logger.Error]:
// - err is optional and may be nil
// - msg is string that describes the problem
// - keysAndValues contains additional information that varies between different occurrences of the problem
//
// [ErrorToString] can be used to convert these parameters into a single string, using the klog text output.
type ErrorHandler func(ctx context.Context, err error, msg string, keysAndValues ...interface{})
// ErrorToString takes the parameters passed to [ErrorHandler] and
// formats them as a string using the klog text output.
//
// If any of the values is a multi-line string, then the resulting
// string also uses line breaks and indention for the sake of readability.
// Does not include a trailing newline.
//
// Use errors.New if an error instead of a string is needed.
func ErrorToString(err error, msg string, keysAndValues ...interface{}) string {
var buffer bytes.Buffer
config := textlogger.NewConfig(
textlogger.Output(&buffer),
textlogger.WithHeader(false),
)
logger := textlogger.NewLogger(config)
logger.Error(err, msg, keysAndValues...)
result := buffer.String()
result = strings.TrimSpace(result)
return result
}
// HandlerError is a method to invoke when a non-user facing piece of code cannot
// return an error and needs to indicate it has been ignored. Invoking this method
// is preferable to logging the error - the default behavior is to log but the

View File

@@ -311,3 +311,39 @@ func TestHandleError(t *testing.T) {
})
}
}
func TestErrorToString(t *testing.T) {
for name, tt := range map[string]struct {
err error
msg string
kvs []any
expectString string
}{
"simple": {
errors.New("some error"),
"Unhandled error",
nil,
`"Unhandled error" err="some error"`,
},
"nil-error": {
nil,
"Some problem occurred",
nil,
`"Some problem occurred"`,
},
"keys-and-values": {
errors.New("some error"),
"Some error occurred",
[]any{"str", "foobar", "int", 1, "multiLine", "line 1\nline 2"},
`"Some error occurred" err="some error" str="foobar" int=1 multiLine=<
line 1
line 2
>`,
},
} {
t.Run(name, func(t *testing.T) {
actualString := ErrorToString(tt.err, tt.msg, tt.kvs...)
assert.Equal(t, tt.expectString, actualString)
})
}
}