Skip to content
On this page

HTTP transports

Sites differ wildly in how hard they are to fetch — plain HTML, JavaScript SPAs, or pages behind aggressive firewalls. The crawler abstracts how a page is fetched behind a transport, selected with:

  • CRAWLER_TRANSPORT — global default in .env;
  • the --transport= flag — also baked into the blueprint;
  • a blueprint's http_config.transport.

This page covers the transports themselves and how to configure each one. How the auto transport detects and escalates past WAFs is its own topic — see Bot protection.

The transports

TransportWhat it doesCostUse for
auto (default)Tries the cheapest transport, and on a detected block identifies the WAF vendor and escalates to the right stronger transport — then bakes the one that worked into the blueprint. See Bot protectionfree*leave it on; most sites just work
guzzlePlain HTTP client (no JS, no anti-bot)freenormal sites, APIs
browserReal headless Chrome via the browserless container (runs JS, real browser fingerprint)freeJS/SPA sites, soft protections
flaresolverrStealth Chromium tuned to solve Cloudflare / DDoS-Guard challenges, via the FlareSolverr containerfreeCloudflare-protected sites
scraping_apiA managed provider (ScrapingBee/ZenRows/ScraperAPI/…) that brings residential proxies + anti-bot solvingpaid (your key)the hardest WAFs (Akamai, PerimeterX)

* auto only escalates to scraping_api when a key is configured; otherwise it stops with an honest message instead of producing a broken robot.

Think of the free rungs as a ladder of effort: guzzle (cheap) → browser (heavier) → flaresolverr (heaviest). Each step up costs you speed and CPU, so you only climb when the cheaper one fails — which is exactly what auto does for you. For a side-by-side speed comparison of the free transports and how the escalation works, see Bot protection.

Self-hosted transports (browserless & FlareSolverr)

Both are optional free Docker services. They are not required to install the package — only when you use the browser, flaresolverr, or auto transports.

Reference demo repo (docker-compose.yml):

bash
docker compose up -d browserless flaresolverr   # start when scraping protected sites
docker compose stop browserless flaresolverr    # free RAM/CPU when done (they run a full Chromium)

Package users (minimal services only):

bash
docker compose -f vendor/datahelm/crawler/docker/compose.services.yml up -d
# or, from a clone of datahelm/crawler:
docker compose -f docker/compose.services.yml up -d

Full stack — use data-helm/enviroment.

Config lives in config/crawler.php (browser, flaresolverr) with env overrides in .env (BROWSERLESS_URL, FLARESOLVERR_URL, FLARESOLVERR_MAX_TIMEOUT). See the Environment variables reference.

Which transport am I using right now?

Whatever CRAWLER_TRANSPORT is set to (and any robot that baked its own --transport overrides it for that robot). guzzle/browser/flaresolverr are all free; only scraping_api costs money.

Waiting for JS-rendered content (--browser-wait-for)

SPAs (Vue/React/Angular) often serve an empty HTML shell and render rows later — a plain fetch, or even a headless browser that captures too early, sees the loading skeleton instead of the data. --browser-wait-for tells the headless browser what to wait for before capturing the HTML, and implies the browser transport when none is chosen:

bash
# wait for a real data row, not the page skeleton
php artisan datahelm:scrap:generate "https://example.com/candidates" \
  --browser-wait-for=".data-table-row"

Accepts a CSS selector or a Puppeteer keyword (networkidle, domcontentloaded, load). Pick a selector that only exists once the data has loaded — a row or a card, not the empty table container (waiting on the container returns the loader). The value is baked into the blueprint as http_config.browser_wait_for, so the generated robot waits the same way on every run.

Proxy rotation

Two independent proxy mechanisms, one per transport family:

guzzle — list proxies in the blueprint's http_config.proxies; they are rotated round-robin, one per request. Use full URLs, credentials included:

json
"http_config": {
  "proxies": [
    "http://user:pass@proxy1.example.com:8080",
    "http://user:pass@proxy2.example.com:8080"
  ]
}

browser / flaresolverr — the headless services take a single upstream proxy via the CRAWLER_PROXY_URL env var (see the Environment variables reference).

Rotating residential proxies help against WAFs that score IP reputation — but for the hardest vendors (Akamai, PerimeterX) a scraping_api provider bundles proxies and anti-bot solving, which is usually more reliable than home-grown rotation.

Managed scraping API (scraping_api)

The paid rung. Vendor-agnostic — it works with any provider following the GET <service>?url=&key=&flags convention (ScrapingBee, ZenRows, ScraperAPI, …). Below is the full setup with ScrapingBee as the worked example.

1 · Create an account and get your key

Sign up at scrapingbee.com — the free tier includes 1,000 API credits with no credit card, enough to test a robot end to end:

The ScrapingBee homepage: "The Best Web Scraping API to Avoid Getting Blocked" — sign-up buttons and a note offering 1,000 free scraper API credits with no credit card required

Once registered, your API key is shown at the top of the dashboard — copy it:

The ScrapingBee dashboard's request builder: the "Your API Key" field in the top-right corner is highlighted with an arrow — this is the value that goes into SCRAPING_API_KEY

2 · Configure .env

Add the provider block to your Laravel .env. These values sit dormant — the paid API is not used globally; every robot keeps using its own free transport unless it was explicitly generated with --transport=scraping_api:

A .env file with the ScrapingBee block filled in: SCRAPING_API_URL=https://app.scrapingbee.com/api/v1/, SCRAPING_API_KEY, SCRAPING_API_KEY_PARAM=api_key, SCRAPING_API_URL_PARAM=url, SCRAPING_API_PARAMS=render_js=true&premium_proxy=true&country_code=br — with comments explaining the API stays dormant until a robot opts in

ini
SCRAPING_API_URL=https://app.scrapingbee.com/api/v1/
SCRAPING_API_KEY=your_scrapingbee_key
SCRAPING_API_KEY_PARAM=api_key
SCRAPING_API_URL_PARAM=url
SCRAPING_API_PARAMS=render_js=true&premium_proxy=true&country_code=br

SCRAPING_API_PARAMS is passed through to the provider as-is — render_js, premium_proxy and country_code here are ScrapingBee flags; each provider has its own. Ready-made blocks for ZenRows and ScraperAPI ship in config/crawler.php.

3 · Generate a robot that opts in

Pass --transport=scraping_api at generate time to route this one robot through the paid API:

bash
docker compose run --rm artisan datahelm:scrap:generate \
  "https://www.example-site.com" \
  --get-detail=true \
  --max-pages=30 \
  --get-primary-image=true --hash-names=true \
  --robot-name=example-site \
  --transport=scraping_api \
  --force

The generated robot's blueprint bakes the choice into http_config:

json
"http_config": {
    "timeout": 60,
    "delay_ms": 300,
    "retry_count": 3,
    "retry_delay_ms": 1000,
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
    "headers": [],
    "proxies": [],
    "cookies": [],
    "render_js": false,
    "browser_wait_for": "",
    "transport": "scraping_api"
}

That last line — "transport": "scraping_api" — is the whole opt-in. Every page this robot fetches now goes through ScrapingBee (https://app.scrapingbee.com/api/v1/?url=…&api_key=… plus your SCRAPING_API_PARAMS flags), so the provider handles the proxies, browser fingerprints and anti-bot solving. Every other robot still uses its own baked-in free transport, and your key is only spent where you chose to spend it.

Two things to keep in mind:

  • Every request costs credits — including pagination and detail pages. That's why the example caps the crawl with --max-pages=30; --limit on each run helps too.
  • render_js: false in http_config refers to the crawler's own headless browser (browserless). JavaScript rendering by the provider is controlled by their flag in SCRAPING_API_PARAMS (render_js=true for ScrapingBee) instead.

Next: Bot protection →

Released under the MIT License.