Add Cloudflare 403 fallback via headless Chrome
All checks were successful
Release / build (push) Successful in 1m40s

Plain HTTP requests to claude.ai get blocked by Cloudflare JS challenges
(403). The fetcher now falls back to headless Chrome using the persistent
browser profile, which can solve challenges natively and reuses existing
cf_clearance cookies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
calic
2026-03-21 00:13:21 +01:00
parent 47165ce02c
commit ba3b73c3dd
3 changed files with 119 additions and 15 deletions

View File

@@ -4,11 +4,13 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"sync"
"time"
"git.davoryn.de/calic/claude-statusline/internal/browser"
"git.davoryn.de/calic/claude-statusline/internal/config"
)
@@ -56,17 +58,36 @@ func doRequest(url, sessionKey string) ([]byte, int, error) {
return body, resp.StatusCode, nil
}
// fetchWithFallback tries a plain HTTP request first, then falls back to
// headless Chrome (which can solve Cloudflare JS challenges) on 403.
func fetchWithFallback(url, sessionKey string) ([]byte, error) {
body, status, err := doRequest(url, sessionKey)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
if status == 200 {
return body, nil
}
if status == 401 {
return nil, fmt.Errorf("auth_expired")
}
if status == 403 {
// Likely a Cloudflare JS challenge — fall back to headless Chrome
log.Printf("HTTP 403 for %s, falling back to headless Chrome", url)
chromeBody, chromeErr := browser.FetchViaChrome(url)
if chromeErr != nil {
return nil, fmt.Errorf("auth_expired") // treat as auth failure if Chrome also fails
}
return chromeBody, nil
}
return nil, fmt.Errorf("HTTP %d", status)
}
// DiscoverOrgID fetches the first organization UUID from the API.
func DiscoverOrgID(sessionKey string) (string, error) {
body, status, err := doRequest(apiBase+"/api/organizations", sessionKey)
body, err := fetchWithFallback(apiBase+"/api/organizations", sessionKey)
if err != nil {
return "", fmt.Errorf("request failed: %w", err)
}
if status == 401 || status == 403 {
return "", fmt.Errorf("auth_expired")
}
if status != 200 {
return "", fmt.Errorf("HTTP %d", status)
return "", err
}
var orgs []struct {
@@ -96,16 +117,13 @@ func FetchUsage(sessionKey, orgID string) (*CacheData, string, error) {
}
url := fmt.Sprintf("%s/api/organizations/%s/usage", apiBase, orgID)
body, status, err := doRequest(url, sessionKey)
body, err := fetchWithFallback(url, sessionKey)
if err != nil {
if err.Error() == "auth_expired" {
return &CacheData{Error: "auth_expired", Status: 403}, orgID, err
}
return &CacheData{Error: "fetch_failed", Message: err.Error()}, orgID, err
}
if status == 401 || status == 403 {
return &CacheData{Error: "auth_expired", Status: status}, orgID, fmt.Errorf("auth_expired")
}
if status != 200 {
return &CacheData{Error: "api_error", Status: status}, orgID, fmt.Errorf("HTTP %d", status)
}
var data CacheData
if err := json.Unmarshal(body, &data); err != nil {