82 lines
3.2 KiB
Markdown
82 lines
3.2 KiB
Markdown
# Browser Gateway Pattern
|
|
|
|
> Built 2026-07-29 for gorouter.app. Use when API keys are broken but browser sessions work.
|
|
|
|
## Problem
|
|
|
|
API service returns 401 "Invalid token" for all API keys (even freshly created ones), but the browser playground works fine with session cookies. Direct HTTP requests with cookies get blocked by Cloudflare (CF 1010). Need a working API endpoint.
|
|
|
|
## Solution: Playwright-Based API Proxy
|
|
|
|
Run a long-lived Playwright browser with an authenticated session. Route incoming API requests through `page.evaluate()` → `fetch()` — the request originates from the browser context, bypassing Cloudflare.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Client (curl) → localhost:8777/v1/chat/completions
|
|
↓
|
|
aiohttp handler picks an active Playwright page
|
|
↓
|
|
page.evaluate(fetch(gourouter.app/v1/chat/completions))
|
|
↓
|
|
response returned to client
|
|
```
|
|
|
|
## Core Code
|
|
|
|
```python
|
|
async def api_request(self, page, method, path, body=None):
|
|
"""Make API request through browser context"""
|
|
js_code = f"""
|
|
(async () => {{
|
|
const headers = {{ 'Content-Type': 'application/json' }};
|
|
const opts = {{ method: '{method}', headers }};
|
|
if (body) opts.body = JSON.stringify(body);
|
|
const resp = await fetch('https://gorouter.app{path}', opts);
|
|
const text = await resp.text();
|
|
return JSON.stringify({{
|
|
status: resp.status,
|
|
body: text,
|
|
headers: Object.fromEntries(resp.headers.entries()),
|
|
}});
|
|
}})()
|
|
"""
|
|
for attempt in range(3):
|
|
try:
|
|
result = await page.evaluate(js_code)
|
|
return json.loads(result)
|
|
except Exception as e:
|
|
if attempt < 2:
|
|
await asyncio.sleep(2)
|
|
return {"status": 0, "body": "all attempts failed"}
|
|
```
|
|
|
|
## Session Management
|
|
|
|
- One `BrowserContext` per authenticated user
|
|
- Login via GitHub OAuth + 2FA TOTP (same flow as auto-reg)
|
|
- Check liveness with `await page.evaluate("1")` before each request
|
|
- Remove dead contexts on failure
|
|
|
|
## Key Design Decisions
|
|
|
|
1. **Playwright, not raw HTTP**: Cloudflare blocks non-browser User-Agents. `page.evaluate()` runs in the real browser context.
|
|
2. **aiohttp for the proxy server**: Lightweight async HTTP. One browser instance, multiple routes.
|
|
3. **Session pool, not one-session-per-request**: Login is slow (~60s). Keep sessions alive and reuse.
|
|
4. **3 retries on fetch failures**: Browser internals can flake; retry before giving up.
|
|
|
|
## Pitfalls
|
|
|
|
- **Browser sessions expire**: Gorouter cookies last ~30 min. Gateway needs periodic re-login.
|
|
- **One browser = one IP**: All proxied requests share the same browser fingerprint and IP. Rate limits apply.
|
|
- **Memory**: Each Playwright page consumes ~50-100MB. Pool size is limited.
|
|
- **Not for production**: This is a workaround for broken API keys. Replace with real API keys when the service fixes auth.
|
|
|
|
## Full Reference Implementation
|
|
|
|
See `gorouter_gateway.py` for the complete implementation with:
|
|
- `/login` endpoint to add sessions
|
|
- `/v1/chat/completions` — OpenAI-compatible
|
|
- `/v1/models` — model list
|
|
- `/health` — session status
|