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 andView on GitHub (pinned to cfe32b8be6)
Solutions
- Copy the quoted body out of the message: an HTML page or plain-text blob immediately identifies a proxy or gateway in the middle
- Point the client directly at the official API host or fix the gateway to pass responses through unmodified
- Reproduce with curl against the same URL and the same API key to compare raw bodies
- 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
- Route client traffic directly to api.tailscale.com or a verified pass-through gateway
- Log the quoted body from this error; it identifies intercepting proxies immediately
- Pin expectations with an integration test that asserts the envelope decodes
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
- tailscale.ACL: %w
- tailscale.ACLHuJSON: %w
- tailscale.SetACL: %w
- tailscale.SetACLHuJSON: %w
- tailscale.PreviewACLForUser: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/939c757a46ad7987.
Report an issue: GitHub.