Amazon Scraper API vs Proxy: Choose a Product Data Source
Compare seller APIs, licensed Amazon product data and proxy page checks by record, access scope and reuse rights.
Read guideSet a Guzzle HTTP proxy, control NO_PROXY behavior, and verify the exit IP. Includes timeout, authentication, and response checks.

To send a Guzzle request through a proxy, set the proxy request option to an explicit HTTP proxy URI and stream the body through a local byte cap, so one oversized response cannot burn the traffic budget. Accept the result only when the same configured client returns the expected exit IP and the page contains the expected marker; a rotating proxy can change the address between requests.
Warning: These requests consume proxy traffic and may incur charges; a body-size check cannot refund bytes already transferred. This example uses an unencrypted HTTP client-to-proxy connection, so credentials can be exposed on that link.
Start with a sandbox or test account, set the provider budget, and use a trusted network. Remove any credential binding that reaches logs.
Audience: PHP developers who know Composer, namespaces, and exceptions.
Tested with PHP 8.4.1 and Guzzle 8.2.0. Install PHP from the official PHP downloads page, then install the exact Guzzle version in the application with Composer:
composer require guzzlehttp/guzzle:8.2.0
Set PROXY_HOST and PROXY_PORT in the process environment; bind optional PROXY_USER and PROXY_PASSWORD from a secret store. The example passes NO_PROXY explicitly because a custom Guzzle proxy option does not automatically inherit that bypass list.
For a local shell, set the non-secret values with export PROXY_HOST=proxy.example.net PROXY_PORT=8080.
Save this as proxy-get.php beside the application's vendor directory. The pinned streaming handler uses a 15-second request option for setup and headers, then a 10-second idle limit for each streamed body read. This single GET follows no redirects and caps streamed response bodies at 1 MiB.
<?php
declare(strict_types=1);
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Handler\StreamHandler;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\ResponseInterface;
const TARGET_URL = 'https://example.com/';
const EXIT_CHECK_URL = 'https://api.ipify.org?format=json';
const EXPECTED_MARKER = 'Example Domain';
const REQUEST_TIMEOUT = 15.0;
const READ_TIMEOUT = 10.0;
const MAX_BODY_BYTES = 1_048_576;
function requiredEnv(string $name): string
{
$value = getenv($name);
if ($value === false || trim($value) === '') {
throw new RuntimeException("Missing environment binding: {$name}");
}
return trim($value);
}
function readBoundedBody(ResponseInterface $response): string
{
$stream = $response->getBody();
$body = '';
try {
while (! $stream->eof()) {
$chunk = $stream->read(min(8192, MAX_BODY_BYTES + 1 - strlen($body)));
if ($chunk === '' && ! $stream->eof()) {
throw new RuntimeException('Response read made no progress');
}
$body .= $chunk;
if (strlen($body) > MAX_BODY_BYTES) {
throw new LengthException('Response exceeded the 1 MiB limit');
}
}
return $body;
} finally {
$stream->close();
}
}
if (! filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN)) {
throw new RuntimeException('Enable allow_url_fopen for the streaming handler');
}
$host = requiredEnv('PROXY_HOST');
$port = filter_var(getenv('PROXY_PORT'), FILTER_VALIDATE_INT);
if ($port === false || $port < 1 || $port > 65535) {
throw new RuntimeException('PROXY_PORT must be a TCP port from 1 through 65535');
}
$user = getenv('PROXY_USER');
$password = getenv('PROXY_PASSWORD');
if (($user === false) !== ($password === false)) {
throw new RuntimeException('Set both proxy credential bindings or neither');
}
$proxy = 'http://';
if ($user !== false) {
$proxy .= rawurlencode($user).':'.rawurlencode($password).'@';
}
$proxy .= $host.':'.$port;
$noProxy = array_values(array_filter(array_map('trim', explode(',', getenv('NO_PROXY') ?: ''))));
$client = new Client([
'allow_redirects' => false,
'handler' => HandlerStack::create(new StreamHandler()),
'http_errors' => false,
'proxy' => ['http' => $proxy, 'https' => $proxy, 'no' => $noProxy],
'timeout' => REQUEST_TIMEOUT,
]);
foreach ([TARGET_URL, EXIT_CHECK_URL] as $url) {
try {
$response = $client->request('GET', $url, [
'read_timeout' => READ_TIMEOUT,
'stream' => true,
]);
} catch (Throwable) {
throw new RuntimeException('Request failed during transport or proxy negotiation');
}
$status = $response->getStatusCode();
try {
$body = readBoundedBody($response);
} catch (LengthException) {
throw new RuntimeException('Response exceeded the 1 MiB limit');
} catch (Throwable) {
throw new RuntimeException('Response read failed; check the read timeout and stream');
}
if ($status !== 200) {
throw new RuntimeException("Request returned HTTP {$status}");
}
if ($url === TARGET_URL && ! str_contains($body, EXPECTED_MARKER)) {
throw new RuntimeException('Target marker was absent');
}
if ($url === EXIT_CHECK_URL) {
$ipResult = json_decode($body, true);
$exitIp = is_array($ipResult) ? ($ipResult['ip'] ?? null) : null;
if (! is_string($exitIp) || filter_var($exitIp, FILTER_VALIDATE_IP) === false) {
throw new RuntimeException('IP-check response did not contain a valid IP address');
}
}
printf("status=%d body_bytes=%d\n", $status, strlen($body));
if ($url === EXIT_CHECK_URL) {
printf("exit_ip=%s\n", $exitIp);
}
}
Run the script from the project root:
php proxy-get.php
The example builds the proxy URI in process memory. It replaces caught transport and stream exceptions with fixed messages that omit that URI and its credentials; an uncaught failure still prints the controlled message and a PHP stack trace. On success the example sends two application GET requests and follows no redirects; it makes no application-level retries.
With stream=true, the pinned 8.2.0 StreamHandler returns the body before consuming it. read_timeout limits each idle read; the request timeout does not impose a deadline on subsequent body consumption.
A slow stream can therefore outlive 15 seconds. Use an external worker deadline when a total duration limit is required. The 1 MiB limit is a local acceptance cap, not a provider traffic quota.
Illustrative output. The IP is replaced with a documentation address; body byte counts can vary. HTTP 200 means the request succeeded:
status=200 body_bytes=559
status=200 body_bytes=23
exit_ip=203.0.113.10
| Observation | Likely boundary | Next check |
|---|---|---|
| Transfer fails before an HTTP response | Client, proxy, TLS, or CONNECT | Check proxy scheme, host, port, and credentials without dumping the exception |
| HTTP 407 | Proxy authentication | Repair proxy credentials or entitlement; do not retry the same secret |
| HTTP 401 or 403 | Target policy | Check target-side login or permission |
| HTTP 502, 503, or 504 | Gateway or target availability | Defer; retry only under an explicit bounded policy that honors Retry-After |
| HTTP 200 without the marker | Application validation | Reject the response and inspect content assumptions |
| Body exceeds 1 MiB | Local resource policy | Reject it or design a different bounded stream policy |
The IP-check response shows the address seen for that request. It does not establish geographic accuracy or a stable exit across rotating requests. See the curl proxy guide for inspecting a CONNECT tunnel and the HTTP 407 guide for proxy challenges.
Scale your workflows with the tools you already use. Rotating or sticky sessions up to 72 hours.
Compare seller APIs, licensed Amazon product data and proxy page checks by record, access scope and reuse rights.
Read guideChoose the right anti-detect browser proxy guide, map the shared verification steps, and keep account access separate from the network route.
Read guideOur picks for market research: ProxyLane for small, intermittent projects, plus options for regional tracking, monthly collection and managed extraction.
Read guide