{"record":{"id":"939c757a46ad7987","repo":"tailscale/tailscale","slug":"json-unmarshal-q-w","errorCode":null,"errorMessage":"json.Unmarshal %q: %w","messagePattern":"json\\.Unmarshal %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/tailscale/acl.go","lineNumber":150,"sourceCode":"\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Accept\", \"application/hujson\")\n\tb, resp, err := c.sendRequest(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, HandleErrorResponse(b, resp)\n\t}\n\n\tdata := struct {\n\t\tACL      []byte   `json:\"acl\"`\n\t\tWarnings []string `json:\"warnings\"`\n\t}{}\n\tif err := json.Unmarshal(b, &data); err != nil {\n\t\treturn nil, fmt.Errorf(\"json.Unmarshal %q: %w\", b, err)\n\t}\n\n\tacl = &ACLHuJSON{\n\t\tACL:      string(data.ACL),\n\t\tWarnings: data.Warnings,\n\t\tETag:     resp.Header.Get(\"ETag\"),\n\t}\n\treturn acl, nil\n}\n\n// ACLTestFailureSummary specifies a user for which ACL tests\n// failed and the related user-friendly error messages.\n//\n// ACLTestFailureSummary specifies the JSON format sent to the\n// JavaScript client to be rendered in the HTML.\ntype ACLTestFailureSummary struct {\n\t// User is the source (\"src\") value of the ACL test that failed.\n\t// The name \"user\" is a legacy holdover from the original naming and","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/client/tailscale/acl.go#L132-L168","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isEnvelopeParseFailure(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"json.Unmarshal\")\n}","tryCatchPattern":"if _, err := c.ACLHuJSON(ctx); err != nil {\n    var er tailscale.ErrResponse\n    if errors.As(err, &er) {\n        return err // server-reported API error: trust it\n    }\n    if isEnvelopeParseFailure(err) {\n        // 200 body was not the JSON envelope: suspect proxy/gateway interception; compare raw bodies via curl\n        return fmt.Errorf(\"response was not the expected JSON envelope (proxy rewrite?): %w\", err)\n    }\n    return err\n}","preventionTips":["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"],"tags":["go","json","api","tailscale","acl","proxy"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}