{"record":{"id":"c1a9bb47849bbcb8","repo":"sipeed/picoclaw","slug":"token-is-required","errorCode":null,"errorMessage":"token is required","messagePattern":"token is required","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/oauth.go","lineNumber":211,"sourceCode":"\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tmethod := strings.ToLower(strings.TrimSpace(req.Method))\n\tif !isOAuthMethodSupported(provider, method) {\n\t\thttp.Error(\n\t\t\tw,\n\t\t\tfmt.Sprintf(\"unsupported login method %q for provider %q\", method, provider),\n\t\t\thttp.StatusBadRequest,\n\t\t)\n\t\treturn\n\t}\n\n\tswitch method {\n\tcase oauthMethodToken:\n\t\ttoken := strings.TrimSpace(req.Token)\n\t\tif token == \"\" {\n\t\t\thttp.Error(w, \"token is required\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tcred := &auth.AuthCredential{\n\t\t\tAccessToken: token,\n\t\t\tProvider:    provider,\n\t\t\tAuthMethod:  oauthMethodToken,\n\t\t}\n\t\tif err := h.persistCredentialAndConfig(provider, oauthMethodToken, cred); err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"token login failed: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(map[string]any{\n\t\t\t\"status\":   \"ok\",\n\t\t\t\"provider\": provider,\n\t\t\t\"method\":   method,","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L193-L229","documentation":"Returned as HTTP 400 by POST /api/oauth/login when method is \"token\" and the token field is empty after trimming. This is the only required field for token login — provider and method were already validated. Whitespace-only tokens are rejected because the value is strings.TrimSpace'd before the check.","triggerScenarios":"POST /api/oauth/login with {\"provider\":\"anthropic\",\"method\":\"token\",\"token\":\"\"} or {\"token\":\"   \"} or with the field named differently (e.g. \"api_key\" or \"accessToken\") so the struct field stays zero.","commonSituations":"Frontend form with an empty paste field; token stored under a different key in state and serialized as token: undefined (dropped by JSON.stringify); users pasting a newline-only string; API clients built from the Anthropic docs that send an Authorization header instead of a body token.","solutions":["Include a non-empty, non-whitespace \"token\" string in the JSON body: {\"provider\":\"anthropic\",\"method\":\"token\",\"token\":\"sk-ant-...\"}.","Trim the token client-side before submitting (token.trim()) so whitespace-only values are caught in the UI.","Check that the JSON key is exactly \"token\", not \"api_key\"/\"access_token\".","Ensure JSON.stringify does not drop the field because the value is undefined — default it or fail the submit."],"exampleFix":"// before\nawait fetch('/api/oauth/login', {method:'POST', body: JSON.stringify({provider, method:'token', token: apiKey || undefined})});\n// undefined is omitted by JSON.stringify -> 400 token is required\n\n// after\nconst token = (apiKey ?? '').trim();\nif (!token) throw new Error('Paste an API token first');\nawait fetch('/api/oauth/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({provider, method:'token', token})});","handlingStrategy":"validation","validationCode":"function assertToken(token) {\n  const t = String(token ?? '').trim();\n  if (!t) throw new Error('token is required');\n  return t;\n}","typeGuard":"function hasToken(v) { return typeof v === 'string' && v.trim().length > 0; }","tryCatchPattern":null,"preventionTips":["Trim the pasted token in the UI and disable submit when empty.","JSON.stringify drops undefined fields — default the field or block submit so the key is always present.","Use the exact JSON key 'token', not 'api_key' or 'access_token'."],"tags":["oauth","http-400","token","validation"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}