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

76
internal/browser/fetch.go Normal file
View File

@@ -0,0 +1,76 @@
package browser
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/chromedp/chromedp"
"git.davoryn.de/calic/claude-statusline/internal/config"
)
// FetchViaChrome navigates to a URL using headless Chrome with the persistent
// browser profile (which has Cloudflare clearance cookies) and returns the
// response body. This bypasses Cloudflare JS challenges because Chrome runs
// real JavaScript. Falls back to non-headless if headless fails.
func FetchViaChrome(url string) ([]byte, error) {
profileDir := filepath.Join(config.ConfigDir(), "browser-profile")
if err := os.MkdirAll(profileDir, 0o755); err != nil {
return nil, fmt.Errorf("create browser profile dir: %w", err)
}
// Remove stale lock file from unclean shutdown
_ = os.Remove(filepath.Join(profileDir, "SingletonLock"))
execPath := findBrowserExec()
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.UserDataDir(profileDir),
)
if execPath != "" {
opts = append(opts, chromedp.ExecPath(execPath))
}
allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer allocCancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Set a total timeout for the operation
ctx, timeoutCancel := context.WithTimeout(ctx, 30*time.Second)
defer timeoutCancel()
var body string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// Wait for the body to have content (Cloudflare challenge resolves via JS)
chromedp.WaitReady("body"),
// Chrome renders JSON API responses inside a <pre> tag
chromedp.Text("pre", &body, chromedp.ByQuery, chromedp.NodeVisible),
)
if err != nil {
// Fallback: try extracting from body directly (some responses may not use <pre>)
var bodyFallback string
errFb := chromedp.Run(ctx,
chromedp.Text("body", &bodyFallback, chromedp.ByQuery),
)
if errFb == nil && bodyFallback != "" {
return []byte(bodyFallback), nil
}
return nil, fmt.Errorf("chromedp fetch: %w", err)
}
if body == "" {
return nil, fmt.Errorf("chromedp fetch: empty response body")
}
// Gracefully close to flush cookies (including refreshed cf_clearance)
cancel()
return []byte(body), nil
}