sipeed/picoclaw · error

flow does not support polling

Error message

flow does not support polling

What it means

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.

Source

Thrown at web/backend/api/oauth.go:352

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(flowToResponse(flow))
}

func (h *Handler) handlePollOAuthFlow(w http.ResponseWriter, r *http.Request) {
	flowID := strings.TrimSpace(r.PathValue("id"))
	if flowID == "" {
		http.Error(w, "missing flow id", http.StatusBadRequest)
		return
	}

	flow, ok := h.getOAuthFlow(flowID)
	if !ok {
		http.Error(w, "flow not found", http.StatusNotFound)
		return
	}

	if flow.Method != oauthMethodDeviceCode {
		http.Error(w, "flow does not support polling", http.StatusBadRequest)
		return
	}
	if flow.Status != oauthFlowPending {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(flowToResponse(flow))
		return
	}

	cfg := auth.OpenAIOAuthConfig()
	cred, err := oauthPollDeviceCodeOnce(cfg, flow.DeviceAuthID, flow.UserCode)
	if err != nil {
		if strings.Contains(strings.ToLower(err.Error()), "pending") {
			updated, _ := h.getOAuthFlow(flowID)
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(flowToResponse(updated))
			return
		}
		h.setOAuthFlowError(flowID, fmt.Sprintf("device code poll failed: %v", err))

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Only start a poll loop for flows created with method "device_code"; remember the method you used to create the flow.
  2. 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.
  3. If the popup/callback failed, recover by GET /api/oauth/flows/{id} — do not POST poll, which will always 400 for browser flows.

Example fix

// before
const timer = setInterval(() => fetch(`/api/oauth/flows/${flowId}/poll`, {method:'POST'}), interval);
// browser flows -> 400 flow does not support polling

// after
if (method === 'device_code') {
  setInterval(() => fetch(`/api/oauth/flows/${flowId}/poll`, {method:'POST'}), interval);
} else { // browser
  setInterval(() => fetch(`/api/oauth/flows/${flowId}`), 2000);
}
Defensive patterns

Strategy: validation

Validate before calling

function assertPollable(flow) {
  if (flow?.method !== 'device_code') throw new Error('flow does not support polling');
  return flow;
}

Type guard

function isPollableFlow(flow) { return flow?.method === 'device_code'; }

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/dbd15c6556bc8429. Report an issue: GitHub.