Hmbown/CodeWhale · error

OAuth callback must be GET

Error message

OAuth callback must be GET

What it means

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.

Solutions

  1. Let the real browser complete the sign-in; do not POST or script the callback URL manually.
  2. Retry `codewhale auth` / sign-in to start a fresh listener on a free port.
  3. Check for local software (antivirus, scanners, other dev servers) hitting the loopback port and exclude the port range.
  4. Ensure the provider's redirect URI uses a plain GET redirect (response_type=code), not form_post.

Example fix

// before (manual probe)
curl -X POST 'http://127.0.0.1:1455/callback?code=abc'
// after
open 'http://127.0.0.1:1455/callback?code=abc'  # or let the browser redirect do a GET
Defensive patterns

Strategy: try-catch

Validate before calling

const isGet = (line) => /^GET\s+\S+\s+HTTP\//.test(line);

Type guard

const isGetRequest = (r) => typeof r.method === 'string' && r.method.toUpperCase() === 'GET';

Try / catch

try { await signIn(); } catch (e) { if (String(e).includes('OAuth callback must be GET')) { /* abort: only browser GET reaches this port */ } }

Prevention

When it happens

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

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

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/d2a2227a2e47e40c. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/oauth.rs:1261

    }
}

fn parse_http_request_target(request_line: &str) -> Result<String> {
    let mut parts = request_line.split_whitespace();
    let method = parts.next().unwrap_or_default();
    anyhow::ensure!(
        method.eq_ignore_ascii_case("GET"),
        "OAuth callback must be GET"
    );
    let target = parts
        .next()
        .context("OAuth callback missing request target")?;
    Ok(target.to_string())
}

fn query_from_target<'a>(params: &OAuthProviderParams, target: &'a str) -> Result<&'a str> {
    let path = target.split('?').next().unwrap_or(target);
    anyhow::ensure!(
        path == params.callback_path,
        "OAuth callback path was not {}",
        params.callback_path
    );
    Ok(target.split_once('?').map(|(_, q)| q).unwrap_or(""))
}

/// Bind the loopback callback on both IP stacks for the first free port.
///
/// The redirect URI has to say `localhost` — that is what is registered with
/// the authorization server, and redirect matching is exact — but `localhost`
/// resolves to `::1` before `127.0.0.1` on IPv6-first hosts. Binding only
/// IPv4 left the browser connecting to a closed port, which browsers paper
/// over with Happy Eyeballs fallback: a working sign-in becomes a slow one,
/// and a broken one wherever that fallback is disabled. Binding both is the
/// fix that keeps the registered redirect URI intact.
///
/// A host with only one stack available binds only that one and still works.

View on GitHub (pinned to 73e0f67d83)