tailscale/tailscale · error
tailscale.Routes: %w
Error message
tailscale.Routes: %w
What it means
Wrapper error from Client.Routes: any failure fetching a device's subnet routes via GET /api/v2/device/<id>/routes is wrapped as 'tailscale.Routes: %w'. Wrapped causes: http.NewRequestWithContext failure, transport error from sendRequest, non-200 converted by HandleErrorResponse (ErrResponse), or JSON decode failure — note netip.Prefix fields fail to unmarshal if the server sends invalid prefixes.
Source
Thrown at client/tailscale/routes.go:30
"fmt"
"net/http"
"net/netip"
)
// Routes contains the lists of subnet routes that are currently advertised by a device,
// as well as the subnets that are enabled to be routed by the device.
type Routes struct {
AdvertisedRoutes []netip.Prefix `json:"advertisedRoutes"`
EnabledRoutes []netip.Prefix `json:"enabledRoutes"`
}
// Routes retrieves the list of subnet routes that have been enabled for a device.
// The routes that are returned are not necessarily advertised by the device,
// they have only been preapproved.
func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.Routes: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.baseURL(), deviceID)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
}View on GitHub (pinned to cfe32b8be6)
Solutions
- errors.As(err, &tailscale.ErrResponse{}) and handle 401/404 specifically.
- Validate the deviceID exists via Client.Devices first.
- Sanitize deviceID (numeric node IDs only) before building the path.
- If a JSON prefix error appears, print the body to spot schema drift.
Example fix
// before
r, err := c.Routes(ctx, deviceID)
if err != nil { return err }
// after
r, err := c.Routes(ctx, deviceID)
if err != nil {
var apiErr tailscale.ErrResponse
if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {
return nil, fmt.Errorf("device %s not found", deviceID)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if deviceID == "" || !isNumeric(deviceID) {
return fmt.Errorf("invalid deviceID %q", deviceID)
}
ids, _ := c.Devices(ctx, nil) // optional existence pre-check Type guard
func isErrResponse(err error) (tailscale.ErrResponse, bool) {
var e tailscale.ErrResponse
return e, errors.As(err, &e)
} Try / catch
r, err := c.Routes(ctx, deviceID)
if err != nil {
var apiErr tailscale.ErrResponse
if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {
return nil, ErrDeviceGone
}
return nil, err
} Prevention
- Use only server-provided numeric device IDs.
- Refresh device lists before acting on stored IDs.
- Classify 404 separately from transport errors.
When it happens
Trigger: Calling Routes with a nonexistent deviceID (404), unauthorized token (401), deviceID containing path-breaking characters (URL parse error — this function does not PathEscape), or a response whose advertised/enabled routes are not parseable netip prefixes.
Common situations: Inventory scripts querying routes for devices already removed; passing a deviceID with a slash/space breaking the URL; control server and client library version skew changing the routes payload shape.
Related errors
- tailscale.Devices: %w
- tailscale.Device: %w
- tailscale.DeleteDevice: %w
- tailscale.DNSConfig: %w
- tailscale.NameServers: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/4f36893c3dc38c8b.
Report an issue: GitHub.