{"record":{"id":"ae51a8a91ad78b23","repo":"tailscale/tailscale","slug":"unmarshal-raw-grants-s-v","errorCode":null,"errorMessage":"unmarshal raw grants %s: %v","messagePattern":"unmarshal raw grants (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"ipn/ipnlocal/peerapi_drive.go","lineNumber":52,"sourceCode":"\t}\n\n\tcapsMap := h.PeerCaps()\n\tdriveCaps, ok := capsMap[peercap.Taildrive]\n\tif !ok {\n\t\th.logf(\"taildrive: not permitted\")\n\t\thttp.Error(w, \"taildrive not permitted\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\trawPerms := make([][]byte, 0, len(driveCaps))\n\tfor _, cap := range driveCaps {\n\t\trawPerms = append(rawPerms, []byte(cap))\n\t}\n\n\tp, err := drive.ParsePermissions(rawPerms)\n\tif err != nil {\n\t\th.logf(\"taildrive: error parsing permissions: %v\", err)\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfs, ok := h.ps.b.sys.DriveForRemote.GetOK()\n\tif !ok {\n\t\th.logf(\"taildrive: not supported on platform\")\n\t\thttp.Error(w, \"taildrive not supported on platform\", http.StatusNotFound)\n\t\treturn\n\t}\n\twr := &httpResponseWrapper{\n\t\tResponseWriter: w,\n\t}\n\tbw := &requestBodyWrapper{\n\t\tReadCloser: r.Body,\n\t}\n\tr.Body = bw\n\n\tdefer func() {","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/ipn/ipnlocal/peerapi_drive.go#L34-L70","documentation":"The drive capability values from the ACL are passed to drive.ParsePermissions, which json.Unmarshals each raw grant into {shares: []string, access: string} (drive/remote_permissions.go:42). A value that is not valid JSON for that shape fails, and the raw error 'unmarshal raw grants <raw>: <detail>' becomes the HTTP 500 body of the peerapi drive endpoint (peerapi_drive.go:52 serves err.Error()).","triggerScenarios":"A policy grant whose app capability for drive is a plain string or number instead of an object, e.g. \"tailscale.com/cap/drive\": [\"rw\"], or an object with wrong-typed fields such as shares not being an array.","commonSituations":"Hand-edited ACL JSON; tooling writing a legacy capability format; copy-paste from documentation of a different version.","solutions":["Inspect the drive capability values in the tailnet policy file.","Rewrite each entry as an object: {\"shares\": [\"*\"], \"access\": \"rw\"} where access is 'ro' or 'rw'.","Apply the corrected policy and retry; the error clears once every grant parses."],"exampleFix":"// before: plain string instead of grant objects\n\"app\": {\"tailscale.com/cap/drive\": [\"rw\"]}\n\n// after: grant objects with shares and access\n\"app\": {\"tailscale.com/cap/drive\": [{\"shares\": [\"*\"], \"access\": \"rw\"}]}","handlingStrategy":"validation","validationCode":"import \"encoding/json\"\n\ntype driveGrant struct {\n    Shares []string `json:\"shares\"`\n    Access string   `json:\"access\"`\n}\n\n// Lint drive grants before applying the policy.\nfunc validDriveGrants(raw []json.RawMessage) error {\n    for _, r := range raw {\n        var g driveGrant\n        if err := json.Unmarshal(r, &g); err != nil {\n            return fmt.Errorf(\"bad drive grant %s: %w\", r, err)\n        }\n        if g.Access != \"ro\" && g.Access != \"rw\" {\n            return fmt.Errorf(\"access must be ro or rw, got %q\", g.Access)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate policy JSON with a lint step before applying.","Use the admin editor's autocomplete instead of hand-writing capability arrays.","Add a CI check that unmarshals every app capability against its expected shape."],"tags":["taildrive","tailscale","acl","json","schema"],"backgroundTag":"capability-schema-invalid","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}