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

  1. Align the client library version with the daemon ('tailscale version') and restart tailscaled
  2. If using headscale or a custom control, upgrade it to emit a DERPMap matching your library's version
  3. Inspect the raw body: curl --unix-socket /var/run/tailscale/tailscaled.sock http://local-tailscaled.sock.localapi.net/localapi/v0/derpmap
  4. 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

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


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