tailscale/tailscale · error
tailscale.Device: %w
Error message
tailscale.Device: %w
What it means
Wrapper error produced by a deferred func in Client.Device: any failure while fetching a single device's details (bad URL construction, transport error, non-200 response, or JSON decode of the Device payload) is wrapped as 'tailscale.Device: %w'. The underlying cause stays inspectable.
Source
Thrown at client/tailscale/devices.go:176
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
}
var devices GetDevicesResponse
err = json.Unmarshal(b, &devices)
return devices.Devices, err
}
// Device retrieved the details for a specific device.
//
// See the Device structure for the list of fields hidden for an external device.
// The optional fields parameter specifies which fields of the devices to return; currently
// only DeviceDefaultFields (equivalent to nil) and DeviceAllFields are supported.
// Other values are currently undefined.
func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFieldsOpts) (device *Device, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.Device: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/device/%s", c.baseURL(), deviceID)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
// Add fields.
fieldStr := fields.addFieldsToQueryParameter()
q := req.URL.Query()
q.Add("fields", fieldStr)
req.URL.RawQuery = q.Encode()
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}View on GitHub (pinned to cfe32b8be6)
Solutions
- Confirm the deviceID currently exists by listing devices with Client.Devices and matching IDs.
- errors.As(err, &tailscale.ErrResponse{}) and handle 404 (device gone), 401 (token), 400 (malformed ID).
- Sanitize/URL-escape deviceID before calling if it may contain odd characters; valid IDs are numeric.
- Check network reachability of the API endpoint if the error is a url.Error.
Example fix
// before
dev, err := c.Device(ctx, deviceID, tailscale.DeviceAllFields)
if err != nil {
return err // opaque 'tailscale.Device: Status: 404, Message: ...'
}
// after
dev, err := c.Device(ctx, deviceID, tailscale.DeviceAllFields)
if err != nil {
var apiErr tailscale.ErrResponse
if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {
return fmt.Errorf("device %s no longer exists: %w", deviceID, err)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate before calling
if expiry < 0 {
expiry = 0 // or return a config error
} Type guard
func validExpiry(d time.Duration) bool { return d >= 0 } Try / catch
secret, meta, err := c.CreateKeyWithExpiry(ctx, caps, d)
if err != nil {
if strings.Contains(err.Error(), "expiry must be positive") {
// recompute d from time.Until(target) and retry once
}
return "", nil, err
} Prevention
- Never derive durations from past timestamps without clamping.
- In tests, use time.Second not time.Millisecond for expiry.
- Centralize key-creation in one helper that validates d >= 0 and (d == 0 || d >= time.Second).
When it happens
Trigger: Calling Client.Device(ctx, deviceID, fields) with a deviceID that does not exist (404 ErrResponse), a deviceID containing characters that break URL formatting (http.NewRequestWithContext parse error), an unauthorized token (401), an external device whose hidden fields produce unexpected JSON, or transport-level failures.
Common situations: Stale device ID cached after the device was removed from the tailnet, using a node ID from a different tailnet, expired API token, or passing an empty deviceID string so the URL path collapses.
Related errors
- tailscale.Devices: %w
- tailscale.DeleteDevice: %w
- tailscale.DNSConfig: %w
- tailscale.NameServers: %w
- tailscale.SearchPaths: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/c373e69528756cbb.
Report an issue: GitHub.