Files
claude-statusline/internal/browser/fetch.go
calic ba3b73c3dd
All checks were successful
Release / build (push) Successful in 1m40s
Add Cloudflare 403 fallback via headless Chrome
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>
2026-03-21 00:13:21 +01:00

77 lines
2.2 KiB
Go

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
}