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.

16Yun Engineering TeamOct 28, 20264 min read

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.server

This 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-demo

Do 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.py

The 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.