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)
		return

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Start a fresh login (POST /api/oauth/login) and use the newly returned flow_id — expired/completed flows cannot be recovered.
  2. Poll within the flow TTL: respect the expires_at field returned with the flow.
  3. Keep the backend process alive during an interactive login; if it restarted, any old flow_id is invalid.
  4. 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

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


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