{"record":{"id":"4f36893c3dc38c8b","repo":"tailscale/tailscale","slug":"tailscale-routes-w","errorCode":null,"errorMessage":"tailscale.Routes: %w","messagePattern":"tailscale\\.Routes: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/tailscale/routes.go","lineNumber":30,"sourceCode":"\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n)\n\n// Routes contains the lists of subnet routes that are currently advertised by a device,\n// as well as the subnets that are enabled to be routed by the device.\ntype Routes struct {\n\tAdvertisedRoutes []netip.Prefix `json:\"advertisedRoutes\"`\n\tEnabledRoutes    []netip.Prefix `json:\"enabledRoutes\"`\n}\n\n// Routes retrieves the list of subnet routes that have been enabled for a device.\n// The routes that are returned are not necessarily advertised by the device,\n// they have only been preapproved.\nfunc (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"tailscale.Routes: %w\", err)\n\t\t}\n\t}()\n\n\tpath := fmt.Sprintf(\"%s/api/v2/device/%s/routes\", c.baseURL(), deviceID)\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", path, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb, resp, err := c.sendRequest(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// If status code was not successful, return the error.\n\t// TODO: Change the check for the StatusCode to include other 2XX success codes.\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, HandleErrorResponse(b, resp)\n\t}","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/client/tailscale/routes.go#L12-L48","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n r, err := c.Routes(ctx, deviceID)\n if err != nil { return err }\n\n// after\n r, err := c.Routes(ctx, deviceID)\n if err != nil {\n     var apiErr tailscale.ErrResponse\n     if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {\n         return nil, fmt.Errorf(\"device %s not found\", deviceID)\n     }\n     return err\n }","handlingStrategy":"try-catch","validationCode":"if deviceID == \"\" || !isNumeric(deviceID) {\n    return fmt.Errorf(\"invalid deviceID %q\", deviceID)\n}\nids, _ := c.Devices(ctx, nil) // optional existence pre-check","typeGuard":"func isErrResponse(err error) (tailscale.ErrResponse, bool) {\n    var e tailscale.ErrResponse\n    return e, errors.As(err, &e)\n}","tryCatchPattern":"r, err := c.Routes(ctx, deviceID)\nif err != nil {\n    var apiErr tailscale.ErrResponse\n    if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {\n        return nil, ErrDeviceGone\n    }\n    return nil, err\n}","preventionTips":["Use only server-provided numeric device IDs.","Refresh device lists before acting on stored IDs.","Classify 404 separately from transport errors."],"tags":["go","tailscale","api-client","subnet-routes","http","error-wrapping"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}