sipeed/picoclaw · warning
flow not found
Error message
flow not found
What it means
Returned as HTTP 404 by GET /api/oauth/flows/{id} when no flow with that id exists in the handler's in-memory map. Flows live only in process memory: pending flows expire after 10 minutes (browser) or 15 minutes (device code), and terminal flows (success/error/expired) are garbage-collected 30 minutes after they finish. A backend restart wipes all flows.
Source
Thrown at web/backend/api/oauth.go:330
"auth_url": authURL,
"expires_at": flow.ExpiresAt.Format(time.RFC3339),
})
return
default:
http.Error(w, "unsupported login method", http.StatusBadRequest)
}
}
func (h *Handler) handleGetOAuthFlow(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
}
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)
returnView on GitHub (pinned to 49183d7e8d)
Solutions
- Start a fresh login (POST /api/oauth/login) and use the newly returned flow_id — expired/completed flows cannot be recovered.
- Poll within the flow TTL: respect the expires_at field returned with the flow.
- Keep the backend process alive during an interactive login; if it restarted, any old flow_id is invalid.
- If you scale the web backend, ensure sticky routing or a single instance owns OAuth flows, since they are in-memory.
Example fix
// before: reusing a stale id after backend restart
const flow = await fetch(`/api/oauth/flows/${oldFlowId}`).then(r => r.json()); // 404 flow not found
// after: restart the flow when 404
let flow = await fetch(`/api/oauth/flows/${flowId}`);
if (flow.status === 404) {
const login = await fetch('/api/oauth/login', {method:'POST', body: JSON.stringify({provider, method})}).then(r => r.json());
flowId = login.flow_id;
flow = await fetch(`/api/oauth/flows/${flowId}`);
} Defensive patterns
Strategy: validation
Try / catch
const res = await fetch(`/api/oauth/flows/${flowId}`);
if (res.status === 404) {
// flow expired, GC'd, or backend restarted: restart login instead of retrying the GET
({ flow_id: flowId } = await startLogin(provider, method));
return pollFlow(flowId);
}
return res.json(); Prevention
- Complete the login within the flow TTL (10 min browser, 15 min device code) using the returned expires_at.
- Keep the backend process alive during interactive logins; restarts wipe in-memory flows.
- Persist the flow_id from the login response and stop querying flows after they reach a terminal status.
When it happens
Trigger: GET /api/oauth/flows/abc123 where the id was never issued; the flow expired and was GC'd; the flow finished more than 30 minutes ago; or the backend process restarted after login started (e.g. hot reload, container restart, redeploy).
Common situations: Leaving the login wizard open in a browser tab overnight then resuming; dev-mode auto-reload restarting the Go binary mid-flow; polling with a flow_id from a previous session; multi-replica setups where the GET lands on a different process than the one holding the flow.
Related errors
- missing flow id
- flow does not support polling
- Invalid browser login response
- Invalid device code response
- error fetching models: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/c6f1e076687fe8c0.
Report an issue: GitHub.