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

  1. Confirm the deviceID currently exists by listing devices with Client.Devices and matching IDs.
  2. errors.As(err, &tailscale.ErrResponse{}) and handle 404 (device gone), 401 (token), 400 (malformed ID).
  3. Sanitize/URL-escape deviceID before calling if it may contain odd characters; valid IDs are numeric.
  4. 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

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


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/c373e69528756cbb. Report an issue: GitHub.