{"record":{"id":"d2a2227a2e47e40c","repo":"Hmbown/CodeWhale","slug":"oauth-callback-must-be-get","errorCode":null,"errorMessage":"OAuth callback must be GET","messagePattern":"OAuth callback must be GET","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":1261,"sourceCode":"    }\n}\n\nfn parse_http_request_target(request_line: &str) -> Result<String> {\n    let mut parts = request_line.split_whitespace();\n    let method = parts.next().unwrap_or_default();\n    anyhow::ensure!(\n        method.eq_ignore_ascii_case(\"GET\"),\n        \"OAuth callback must be GET\"\n    );\n    let target = parts\n        .next()\n        .context(\"OAuth callback missing request target\")?;\n    Ok(target.to_string())\n}\n\nfn query_from_target<'a>(params: &OAuthProviderParams, target: &'a str) -> Result<&'a str> {\n    let path = target.split('?').next().unwrap_or(target);\n    anyhow::ensure!(\n        path == params.callback_path,\n        \"OAuth callback path was not {}\",\n        params.callback_path\n    );\n    Ok(target.split_once('?').map(|(_, q)| q).unwrap_or(\"\"))\n}\n\n/// Bind the loopback callback on both IP stacks for the first free port.\n///\n/// The redirect URI has to say `localhost` — that is what is registered with\n/// the authorization server, and redirect matching is exact — but `localhost`\n/// resolves to `::1` before `127.0.0.1` on IPv6-first hosts. Binding only\n/// IPv4 left the browser connecting to a closed port, which browsers paper\n/// over with Happy Eyeballs fallback: a working sign-in becomes a slow one,\n/// and a broken one wherever that fallback is disabled. Binding both is the\n/// fix that keeps the registered redirect URI intact.\n///\n/// A host with only one stack available binds only that one and still works.","sourceCodeStart":1243,"sourceCodeEnd":1279,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L1243-L1279","documentation":"During local-loopback OAuth sign-in, the TUI runs a tiny HTTP server on the callback port and parses each incoming request line via parse_http_request_target. It rejects any request whose HTTP method is not GET, because the OAuth authorization-code flow delivers the code via a browser GET redirect. Anything else (POST, HEAD, scanner probes) cannot carry a valid authorization response.","triggerScenarios":"A TCP client connects to the loopback callback port and sends a non-GET request line (e.g. \"POST /callback?code=... HTTP/1.1\", \"HEAD /\", or a raw non-HTTP probe) before the real browser redirect arrives.","commonSituations":"Port scanners or security software probing open loopback ports; another local app that claimed the port and speaks its own protocol; a curl POST used to test the callback; a misconfigured redirect that issues POST instead of GET.","solutions":["Let the real browser complete the sign-in; do not POST or script the callback URL manually.","Retry `codewhale auth` / sign-in to start a fresh listener on a free port.","Check for local software (antivirus, scanners, other dev servers) hitting the loopback port and exclude the port range.","Ensure the provider's redirect URI uses a plain GET redirect (response_type=code), not form_post."],"exampleFix":"// before (manual probe)\ncurl -X POST 'http://127.0.0.1:1455/callback?code=abc'\n// after\nopen 'http://127.0.0.1:1455/callback?code=abc'  # or let the browser redirect do a GET","handlingStrategy":"try-catch","validationCode":"const isGet = (line) => /^GET\\s+\\S+\\s+HTTP\\//.test(line);","typeGuard":"const isGetRequest = (r) => typeof r.method === 'string' && r.method.toUpperCase() === 'GET';","tryCatchPattern":"try { await signIn(); } catch (e) { if (String(e).includes('OAuth callback must be GET')) { /* abort: only browser GET reaches this port */ } }","preventionTips":["Never script POST/HEAD requests at the loopback callback port","Exclude the callback port range from local port scanners and probes","Test the callback by opening the URL in a browser, not curl -X POST"],"tags":["oauth","http","network"],"backgroundTag":"invalid-argument-value","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}