tailscale/tailscale · error
invalid derp map json: %w
Error message
invalid derp map json: %w
What it means
Thrown by Client.CurrentDERPMap (client/local/local.go:1163) when GET /localapi/v0/derpmap returned 200 but json.Unmarshal into tailcfg.DERPMap failed. The DERPMap originates from the control plane and is cached by the daemon, so a shape mismatch means version skew between library and daemon or a nonstandard control plane.
Source
Thrown at client/local/local.go:1163
return nil, errors.New("http Transport did not provide a writable body")
}
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
//
// API maturity: this is considered a stable API, though the returned
// [tailcfg.DERPMap] type is subject to minor changes over time; its
// general shape is stable.
func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}
// CertDomains returns the list of domains for which the local tailscaled can
// fetch TLS certificates, equivalent to the DNS.CertDomains field of the
// current netmap. The returned list is sorted in ascending order, and is
// empty if no netmap has been received yet.
//
// API maturity: this is considered a stable API.
func (lc *Client) CertDomains(ctx context.Context) ([]string, error) {
body, err := lc.get200(ctx, "/localapi/v0/cert-domains")
if err != nil {
return nil, err
}
return decodeJSON[[]string](body)
}
View on GitHub (pinned to cfe32b8be6)
Solutions
- Align the client library version with the daemon ('tailscale version') and restart tailscaled
- If using headscale or a custom control, upgrade it to emit a DERPMap matching your library's version
- Inspect the raw body: curl --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock.localapi.net/localapi/v0/derpmap
- Pin the tailscale.com module to the same version as the daemon fleet
Defensive patterns
Strategy: try-catch
Validate before calling
st, err := lc.Status(ctx)
if err != nil { return err }
if st.Version == "" { return errors.New("cannot determine tailscaled version") } Type guard
func isJSONDecodeError(err error) bool {
var se *json.SyntaxError
var te *json.UnmarshalTypeError
return errors.As(err, &se) || errors.As(err, &te)
} Try / catch
dm, err := lc.CurrentDERPMap(ctx)
if err != nil {
if isJSONDecodeError(err) {
// DERPMap shape drifts by design: pin library to daemon version
}
return nil, err
} Prevention
- Remember DERPMap is only shape-stable, not exact: re-pin the module on daemon upgrades
- With headscale, upgrade it in lockstep with the client library
When it happens
Trigger: Daemon and client library from different releases with changed tailcfg.DERPMap fields (DERPMap is explicitly subject to minor shape changes); a headscale or custom control serving a nonstandard derpmap that the daemon passes through verbatim; proxy on the socket.
Common situations: Netcheck-style tooling linked against a newer tailscale.com module than the node's daemon; mixed fleets where a central monitor queries many daemon versions.
Related errors
- invalid JSON from check-udp-gro-forwarding: %w
- invalid JSON from set-udp-gro-forwarding: %w
- invalid prefs JSON: %w
- invalid dns.OSConfig: %w
- invalid query response: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/2733549bd0877abc.
Report an issue: GitHub.