tailscale/tailscale · error

json.Unmarshal %q: %w

Error message

json.Unmarshal %q: %w

What it means

After a 200 from the ACLHuJSON GET, the body must unmarshal into the envelope {"acl": string, "warnings": []string}; this error means it did not, and the full offending body is quoted in the message. Although the endpoint serves HuJSON content, the envelope is strict JSON, so failure usually means the body never came from the real API: an HTML intercept page from a proxy, a rewritten gateway response, or a control-version envelope change.

Source

Thrown at client/tailscale/acl.go:150

	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/hujson")
	b, resp, err := c.sendRequest(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, HandleErrorResponse(b, resp)
	}

	data := struct {
		ACL      []byte   `json:"acl"`
		Warnings []string `json:"warnings"`
	}{}
	if err := json.Unmarshal(b, &data); err != nil {
		return nil, fmt.Errorf("json.Unmarshal %q: %w", b, err)
	}

	acl = &ACLHuJSON{
		ACL:      string(data.ACL),
		Warnings: data.Warnings,
		ETag:     resp.Header.Get("ETag"),
	}
	return acl, nil
}

// ACLTestFailureSummary specifies a user for which ACL tests
// failed and the related user-friendly error messages.
//
// ACLTestFailureSummary specifies the JSON format sent to the
// JavaScript client to be rendered in the HTML.
type ACLTestFailureSummary struct {
	// User is the source ("src") value of the ACL test that failed.
	// The name "user" is a legacy holdover from the original naming and

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Copy the quoted body out of the message: an HTML page or plain-text blob immediately identifies a proxy or gateway in the middle
  2. Point the client directly at the official API host or fix the gateway to pass responses through unmodified
  3. Reproduce with curl against the same URL and the same API key to compare raw bodies
  4. If the body is valid JSON but structurally different, check the current ACL API documentation for envelope changes and update the client
Defensive patterns

Strategy: try-catch

Type guard

func isEnvelopeParseFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "json.Unmarshal")
}

Try / catch

if _, err := c.ACLHuJSON(ctx); err != nil {
    var er tailscale.ErrResponse
    if errors.As(err, &er) {
        return err // server-reported API error: trust it
    }
    if isEnvelopeParseFailure(err) {
        // 200 body was not the JSON envelope: suspect proxy/gateway interception; compare raw bodies via curl
        return fmt.Errorf("response was not the expected JSON envelope (proxy rewrite?): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: A 200 response whose body is not valid JSON: transparent proxies or captive portals injecting HTML, apiBaseUrl pointing at a gateway that rewrites responses, a server-side envelope change, or a type mismatch such as warnings not being a string array.

Common situations: Corporate TLS-inspecting proxies in front of api.tailscale.com; custom gateways mangling response bodies; running the client against an old or experimental control server with a different response shape.

Related errors


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