{"record":{"id":"8fda7a6e161b6bfc","repo":"sipeed/picoclaw","slug":"missing-flow-id","errorCode":null,"errorMessage":"missing flow id","messagePattern":"missing flow id","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"web/backend/api/oauth.go","lineNumber":324,"sourceCode":"\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,\n\t\t\t\"flow_id\":    flow.ID,\n\t\t\t\"auth_url\":   authURL,\n\t\t\t\"expires_at\": flow.ExpiresAt.Format(time.RFC3339),\n\t\t})\n\t\treturn\n\tdefault:\n\t\thttp.Error(w, \"unsupported login method\", http.StatusBadRequest)\n\t}\n}\n\nfunc (h *Handler) handleGetOAuthFlow(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\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","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/oauth.go#L306-L342","documentation":"Returned as HTTP 400 by GET /api/oauth/flows/{id} when the path value trims to empty. Because the route is registered as \"GET /api/oauth/flows/{id}\" on a Go 1.22+ ServeMux, a truly empty segment does not match the pattern at all; you reach this check only when the id consists of whitespace (URL-encoded, e.g. %20) that strings.TrimSpace strips. It is a defensive guard against malformed client-built URLs.","triggerScenarios":"GET /api/oauth/flows/%20 or /api/oauth/flows/%09 — an id that exists in the path but is whitespace-only after decoding/trimming.","commonSituations":"Clients interpolating an unvalidated/undefined flow id into the URL template; template engines emitting a space placeholder; copy-paste artifacts in manual curl tests.","solutions":["Always use the exact flow_id string returned by POST /api/oauth/login in the URL.","Client-side, reject or skip the request when flowId is empty or whitespace before building the URL.","URL-encode the id (encodeURIComponent) so spaces cannot slip in unnoticed."],"exampleFix":"// before\nconst url = `/api/oauth/flows/${flowId ?? ' '}`; // placeholder whitespace -> 400 missing flow id\n\n// after\nif (!flowId?.trim()) throw new Error('No OAuth flow in progress');\nconst url = `/api/oauth/flows/${encodeURIComponent(flowId)}`;","handlingStrategy":"validation","validationCode":"function flowUrl(flowId) {\n  const id = String(flowId ?? '').trim();\n  if (!id) throw new Error('missing flow id');\n  return `/api/oauth/flows/${encodeURIComponent(id)}`;\n}","typeGuard":"function isFlowId(v) { return typeof v === 'string' && v.trim().length > 0; }","tryCatchPattern":null,"preventionTips":["Only build flow URLs from the flow_id returned by POST /api/oauth/login.","Always encodeURIComponent path parameters.","Never substitute placeholders (spaces) for missing ids in URL templates."],"tags":["oauth","http-400","flow","url","validation"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}