FoxProfile: Manage Independent Browser Tasks through REST
Install FoxProfile, create a fixed-engine profile, launch it through authenticated REST calls, collect records, and stop it without deleting persistent identity.
A backend browser job needs more than page actions. It must know which profile it owns, whether another job is using it, and who stops the browser afterward.
This tutorial creates a dedicated catalog profile, launches it, reads a book list, saves JSON, and stops the runtime. Repeating the task reuses the profile rather than creating a new identity.
Prepare the input addresses
Locate the following pages in your browser, confirm their contents, and copy their full addresses. Obtain service and repository addresses from account integration settings, official setup instructions, or the repository clone menu. Do not derive real addresses from illustrative output.
FOXPROFILE_REPO_URL: the clone address of the official chienbm98/foxprofile repository.BOOKS_LIST_URL: the Books to Scrape product-list address.
Set these in the same Bash/WSL terminal used for subsequent commands:
export FOXPROFILE_REPO_URL='<paste-the-actual-foxprofile-repo-url>'
export BOOKS_LIST_URL='<paste-the-actual-books-list-url>'Replace each <VARIABLE_NAME> in JSON or task parameters with the copied value before submission; JSON does not expand environment variables. Any example.16yun.cn source in an output illustration shows field structure, not a demo service deployed for this article.
Install the service and engine
Use Python 3.12, Git, and Bash:
git clone "$FOXPROFILE_REPO_URL" foxprofile
cd foxprofile
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m camoufox fetch
export FOXPROFILE_API_TOKEN='replace-with-at-least-24-random-characters'
python -m src.serverThis installation follows repository code. Retain the actual version and dependencies; the tutorial uses the 2.3-series API structure. Engine downloads and first-use preparation are separate from target-page loading.
The local API documentation is at port 8000's docs page. This example uses Camoufox. Chrome is another engine, and an existing profile's engine cannot be changed in place. See the official project.
Create once and reuse
In another terminal, set the same token and the BOOKS_LIST_URL configured above:
export FOXPROFILE_API_TOKEN='replace-with-the-same-server-token'
curl --fail-with-body -H "Authorization: Bearer $FOXPROFILE_API_TOKEN" -H 'Content-Type: application/json' -X POST http://localhost:8000/api/v1/profiles -d '{"name":"catalog-demo","engine":"camoufox","os_type":"linux","locale":"en-US","timezone":"UTC"}'The operating-system choice assumes Linux; select the actual environment. Fixed language and timezone establish task conditions, not a universal regional configuration.
Inspect the created profile:
curl --fail-with-body -H "Authorization: Bearer $FOXPROFILE_API_TOKEN" http://localhost:8000/api/v1/profiles/catalog-demoDo not recreate it for every run. Its persistent identity differs from a temporary job. Parallel jobs must not manipulate the same active profile; allocate distinct profiles or enforce exclusive scheduling.
Connect the complete lifecycle
Create profile_job.py:
import json
import os
from pathlib import Path
from urllib.request import Request, urlopen
BASE = "http://localhost:8000/api/v1"
TOKEN = os.environ["FOXPROFILE_API_TOKEN"]
NAME = "catalog-demo"
def api(path, payload=None):
request = Request(
BASE + path,
data=None if payload is None else json.dumps(payload).encode(),
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
method="GET" if payload is None else "POST",
)
with urlopen(request, timeout=120) as response:
return json.load(response)
running = False
try:
api(f"/browser/{NAME}/launch", {})
running = True
api(f"/browser/{NAME}/page/navigate", {
"url": os.environ["BOOKS_LIST_URL"],
"wait_until": "domcontentloaded",
})
api(f"/browser/{NAME}/page/wait", {
"selector": "article.product_pod", "timeout": 15000,
})
response = api(f"/browser/{NAME}/page/evaluate", {
"script": """() => ({
source_url: location.href,
rows: [...document.querySelectorAll('article.product_pod')].map(card => ({
title: card.querySelector('h3 a')?.getAttribute('title'),
price_display: card.querySelector('.price_color')?.textContent?.trim(),
detail_url: card.querySelector('h3 a')?.href
}))
})""",
})
result = response["result"]
if not result["rows"] or any(
not row.get("title") or not row.get("price_display")
for row in result["rows"]
):
raise ValueError("商品列表为空或缺少字段")
Path("catalog.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print({"profile": NAME, "records": len(result["rows"])})
finally:
if running:
api(f"/browser/{NAME}/stop", {})Run:
python profile_job.pyThe default launch waits for readiness. A busy-profile conflict should not be treated as permission to take over another job's browser.
The running flag is set only after this job's launch succeeds, so a failed launch does not enter its stop path.
Verify output and reuse
Inspect catalog.json against representative source titles, prices, and links. These are demonstration prices. The example reads one list page and does not claim site-wide coverage.
Query the profile's status endpoint afterward to confirm stopping. Run the same script again to reuse the same profile. A public catalog cannot establish that an authenticated site's cookies will remain valid indefinitely.
For login tasks, establish the session in that profile and define when manual renewal is allowed. Stopping a browser and deleting persistent profile data are different operations; do not delete the profile in cleanup merely to release the runtime.
Manage changes explicitly
Proxy, language, and timezone settings belong to profile identity. Do not change a stable-exit account's route arbitrarily during a job.
A new engine requires a new profile and deliberate migration. Cookie and profile exports are sensitive state, not ordinary crawler output for a public repository.
A network deployment also needs explicit authentication and transport arrangements; the local-loopback example is not a public-service configuration.
Troubleshoot
409 at launch: inspect current ownership and wait or allocate another profile rather than retrying aggressively.
502 at launch: inspect engine preparation, environment, and service logs before page selectors.
401: compare client and server management tokens and Bearer formatting.
Unexpected evaluate shape: check the installed API documentation. This path uses script input and result output, not another library's expression field.
Another job's page appears: repair profile exclusivity. Longer waits cannot isolate concurrent consumers of the same active tab.
A browser remains after an exception: verify the stop request's actual outcome. Executing finally does not prove a remote stop succeeded.
Stable profile identity plus exclusive job ownership makes programmatic browser work repeatable and diagnosable.
Need an enterprise proxy plan?
We can tailor architecture to your target domains, concurrency, and reliability goals.