all: use errors.AsType for type-safe error unwrapping

Replace errors.As with the new errors.AsType generic function
introduced in Go 1.26. This provides compile-time type safety
and approximately 3x better performance by avoiding reflection.

Before:
    var target *AppError
    if errors.As(err, &target) {
        // use target
    }

After:
    if target, ok := errors.AsType[*AppError](err); ok {
        // use target
    }
This commit is contained in:
Kristoffer Dalby 2026-01-20 14:22:27 +00:00
parent 57d9619321
commit 9ab229675d
6 changed files with 20 additions and 29 deletions

View file

@ -23,8 +23,7 @@ var serveCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app, err := newHeadscaleServerWithConfig()
if err != nil {
var squibbleErr squibble.ValidationError
if errors.As(err, &squibbleErr) {
if squibbleErr, ok := errors.AsType[squibble.ValidationError](err); ok {
fmt.Printf("SQLite schema failed to validate:\n")
fmt.Println(squibbleErr.Diff)
}

View file

@ -16,7 +16,6 @@ import (
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/ptr"
)
type AuthProvider interface {
@ -113,8 +112,7 @@ func (h *Headscale) handleRegister(
resp, err := h.handleRegisterWithAuthKey(req, machineKey)
if err != nil {
// Preserve HTTPError types so they can be handled properly by the HTTP layer
var httpErr HTTPError
if errors.As(err, &httpErr) {
if httpErr, ok := errors.AsType[HTTPError](err); ok {
return nil, httpErr
}
@ -316,7 +314,7 @@ func (h *Headscale) reqToNewRegisterResponse(
MachineKey: machineKey,
NodeKey: req.NodeKey,
Hostinfo: hostinfo,
LastSeen: ptr.To(time.Now()),
LastSeen: new(time.Now()),
},
)
@ -344,8 +342,7 @@ func (h *Headscale) handleRegisterWithAuthKey(
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, NewHTTPError(http.StatusUnauthorized, "invalid pre auth key", nil)
}
var perr types.PAKError
if errors.As(err, &perr) {
if perr, ok := errors.AsType[types.PAKError](err); ok {
return nil, NewHTTPError(http.StatusUnauthorized, perr.Error(), nil)
}
@ -443,7 +440,7 @@ func (h *Headscale) handleRegisterInteractive(
MachineKey: machineKey,
NodeKey: req.NodeKey,
Hostinfo: hostinfo,
LastSeen: ptr.To(time.Now()),
LastSeen: new(time.Now()),
},
)

View file

@ -36,8 +36,7 @@ const (
// httpError logs an error and sends an HTTP error response with the given.
func httpError(w http.ResponseWriter, err error) {
var herr HTTPError
if errors.As(err, &herr) {
if herr, ok := errors.AsType[HTTPError](err); ok {
http.Error(w, herr.Msg, herr.Code)
log.Error().Err(herr.Err).Int("code", herr.Code).Msgf("user msg: %s", herr.Msg)
} else {

View file

@ -256,8 +256,7 @@ func (ns *noiseServer) NoiseRegistrationHandler(
resp, err = ns.headscale.handleRegister(req.Context(), regReq, ns.conn.Peer())
if err != nil {
var httpErr HTTPError
if errors.As(err, &httpErr) {
if httpErr, ok := errors.AsType[HTTPError](err); ok {
resp = &tailcfg.RegisterResponse{
Error: httpErr.Msg,
}

View file

@ -16,7 +16,6 @@ import (
"go4.org/netipx"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/multierr"
"tailscale.com/util/slicesx"
@ -656,17 +655,17 @@ func parseAlias(vs string) (Alias, error) {
case isWildcard(vs):
return Wildcard, nil
case isUser(vs):
return ptr.To(Username(vs)), nil
return new(Username(vs)), nil
case isGroup(vs):
return ptr.To(Group(vs)), nil
return new(Group(vs)), nil
case isTag(vs):
return ptr.To(Tag(vs)), nil
return new(Tag(vs)), nil
case isAutoGroup(vs):
return ptr.To(AutoGroup(vs)), nil
return new(AutoGroup(vs)), nil
}
if isHost(vs) {
return ptr.To(Host(vs)), nil
return new(Host(vs)), nil
}
return nil, fmt.Errorf(`Invalid alias %q. An alias must be one of the following types:
@ -829,11 +828,11 @@ func (aa AutoApprovers) MarshalJSON() ([]byte, error) {
func parseAutoApprover(s string) (AutoApprover, error) {
switch {
case isUser(s):
return ptr.To(Username(s)), nil
return new(Username(s)), nil
case isGroup(s):
return ptr.To(Group(s)), nil
return new(Group(s)), nil
case isTag(s):
return ptr.To(Tag(s)), nil
return new(Tag(s)), nil
}
return nil, fmt.Errorf(`Invalid AutoApprover %q. An alias must be one of the following types:
@ -925,11 +924,11 @@ func (o Owners) MarshalJSON() ([]byte, error) {
func parseOwner(s string) (Owner, error) {
switch {
case isUser(s):
return ptr.To(Username(s)), nil
return new(Username(s)), nil
case isGroup(s):
return ptr.To(Group(s)), nil
return new(Group(s)), nil
case isTag(s):
return ptr.To(Tag(s)), nil
return new(Tag(s)), nil
}
return nil, fmt.Errorf(`Invalid Owner %q. An alias must be one of the following types:
@ -2023,8 +2022,7 @@ func unmarshalPolicy(b []byte) (*Policy, error) {
ast.Standardize()
if err = json.Unmarshal(ast.Pack(), &policy, policyJSONOpts...); err != nil {
var serr *json.SemanticError
if errors.As(err, &serr) && serr.Err == json.ErrUnknownName {
if serr, ok := errors.AsType[*json.SemanticError](err); ok && serr.Err == json.ErrUnknownName {
ptr := serr.JSONPointer
name := ptr.LastToken()
return nil, fmt.Errorf("unknown field %q", name)

View file

@ -110,8 +110,7 @@ func TestCanUsePreAuthKey(t *testing.T) {
if err == nil {
t.Errorf("expected error but got none")
} else {
var httpErr PAKError
ok := errors.As(err, &httpErr)
httpErr, ok := errors.AsType[PAKError](err)
if !ok {
t.Errorf("expected HTTPError but got %T", err)
} else {