Single LangGraph graph with one node that uses the official Cloudflare Python library (AsyncCloudflare) to call Browser Rendering content.create. Use it to fetch fully rendered HTML for JavaScript-heavy pages (e.g. Handshake) using your session cookie when needed.
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env # set CF_ACCOUNT_ID, CF_API_TOKEN
cp secrets/handshake_cookies.json.example secrets/handshake_cookies.json
# Paste your real cookies into handshake_cookies.json (file is gitignored)| Approach | When to use |
|---|---|
HANDSHAKE_COOKIES_PATH |
Recommended. JSON file: array of { "name", "value", "domain", "path" } (same shape as browser devtools export / your snippet). Safe for long JWT-style values like hss-global. |
HANDSHAKE_COOKIE |
One-line Cookie header (a=b; c=d) for quick tests. |
| Graph state | cookies (list) or cookies_path (string) when invoking the graph from code. |
If structured cookies are loaded from file or state, the node sends them via Cloudflare’s cookies API field. The auto HANDSHAKE_COOKIE header is skipped in that case to avoid duplicates; you can still set a Cookie header manually in extra_http_headers if you need both.
python -m server --url "https://example.com"
python -m server --url "https://nyu.joinhandshake.com"- Set
HANDSHAKE_COOKIES_PATHin.envto yourhandshake_cookies.json(see setup above). --wait-until load(default) — Handshake and similar SPAs often never reachnetworkidle0, which triggers a 30s navigation timeout; useloadordomcontentloaded, ornetworkidle2if you need stricter idleness.--timeout-ms 60000(default, max allowed) — raises Cloudflare’s navigation timeout from the API default (30s).--no-goto-optionsuses API defaults (short timeout — often fails on heavy SPAs).--jsonprints the final state as JSON (truncates very large HTML).
Console script: apper-render (same flags).
Tracing is off by default. To send LangGraph runs to LangSmith:
- Set in
.env(see.env.example):LANGCHAIN_TRACING_V2=trueLANGCHAIN_API_KEY=(orLANGSMITH_API_KEY)LANGCHAIN_PROJECT=apper(orLANGSMITH_PROJECT)
- Optional:
LANGCHAIN_ENDPOINT/LANGSMITH_ENDPOINT(e.g. EU host).
build_graph() calls configure_langsmith(), which copies these settings into os.environ so LangChain/LangGraph pick them up. You can also call configure_langsmith() yourself before invoking the graph if you load settings another way.
import asyncio
from server.graph import build_graph
async def main():
graph = build_graph()
return await graph.ainvoke(
{
"url": "https://…",
"extra_http_headers": {"Cookie": "…"},
"goto_options": {"wait_until": "load", "timeout": 60000},
}
)
asyncio.run(main())server/graph.py—StateGraph:START → browser_render → ENDserver/nodes/browser_render.py— Cloudflare/contentPOSTserver/state.py—GraphStateserver/config.py— env-based settings (incl. LangSmith)server/tracing.py—configure_langsmith()→os.environ
- Session cookies expire; rotate
HANDSHAKE_COOKIEas needed. - Respect Cloudflare quotas and Handshake terms of use.