{"record":{"id":"dbd15c6556bc8429","repo":"sipeed/picoclaw","slug":"flow-does-not-support-polling","errorCode":null,"errorMessage":"flow does not support polling","messagePattern":"flow does not support polling","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/oauth.go","lineNumber":352,"sourceCode":"\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t_ = json.NewEncoder(w).Encode(flowToResponse(flow))\n}\n\nfunc (h *Handler) handlePollOAuthFlow(w http.ResponseWriter, r *http.Request) {\n\tflowID := strings.TrimSpace(r.PathValue(\"id\"))\n\tif flowID == \"\" {\n\t\thttp.Error(w, \"missing flow id\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tflow, ok := h.getOAuthFlow(flowID)\n\tif !ok {\n\t\thttp.Error(w, \"flow not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tif flow.Method != oauthMethodDeviceCode {\n\t\thttp.Error(w, \"flow does not support polling\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif flow.Status != oauthFlowPending {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(flowToResponse(flow))\n\t\treturn\n\t}\n\n\tcfg := auth.OpenAIOAuthConfig()\n\tcred, err := oauthPollDeviceCodeOnce(cfg, flow.DeviceAuthID, flow.UserCode)\n\tif err != nil {\n\t\tif strings.Contains(strings.ToLower(err.Error()), \"pending\") {\n\t\t\tupdated, _ := h.getOAuthFlow(flowID)\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t_ = json.NewEncoder(w).Encode(flowToResponse(updated))\n\t\t\treturn\n\t\t}\n\t\th.setOAuthFlowError(flowID, fmt.Sprintf(\"device code poll failed: %v\", err))","sourceCodeStart":334,"sourceCodeEnd":370,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L334-L370","documentation":"Returned as HTTP 400 by POST /api/oauth/flows/{id}/poll when the flow exists but its Method is not \"device_code\". Polling is implemented exclusively for the device-code grant; browser flows complete via the /oauth/callback redirect (which exchanges the code and marks the flow success), so there is nothing to poll. The check runs before the pending-status check, so even a completed browser flow hit with POST returns this 400.","triggerScenarios":"POST /api/oauth/flows/<id>/poll where <id> was created by POST /api/oauth/login {\"provider\":\"openai\",\"method\":\"browser\"} or {\"provider\":\"google-antigravity\",\"method\":\"browser\"}. Any poll of a browser flow, pending or finished.","commonSituations":"Frontend using one generic poll loop for all login methods; refactoring that routes the browser flow's id into the device-code poller; polling a browser flow because the callback page never reached the opener (popup blocked).","solutions":["Only start a poll loop for flows created with method \"device_code\"; remember the method you used to create the flow.","For browser flows, open the auth_url and wait for the /oauth/callback redirect (the page postMessages the result to the opener); use GET /api/oauth/flows/{id} to check status instead of POST poll.","If the popup/callback failed, recover by GET /api/oauth/flows/{id} — do not POST poll, which will always 400 for browser flows."],"exampleFix":"// before\nconst timer = setInterval(() => fetch(`/api/oauth/flows/${flowId}/poll`, {method:'POST'}), interval);\n// browser flows -> 400 flow does not support polling\n\n// after\nif (method === 'device_code') {\n  setInterval(() => fetch(`/api/oauth/flows/${flowId}/poll`, {method:'POST'}), interval);\n} else { // browser\n  setInterval(() => fetch(`/api/oauth/flows/${flowId}`), 2000);\n}","handlingStrategy":"validation","validationCode":"function assertPollable(flow) {\n  if (flow?.method !== 'device_code') throw new Error('flow does not support polling');\n  return flow;\n}","typeGuard":"function isPollableFlow(flow) { return flow?.method === 'device_code'; }","tryCatchPattern":null,"preventionTips":["Branch on the method used at login: device_code → POST poll; browser → GET status and wait for the callback.","Do not route browser flow ids into a generic poll loop."],"tags":["oauth","http-400","polling","browser-oauth","flow"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}