> ## Documentation Index
> Fetch the complete documentation index at: https://help.miyaip.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Playwright proxy setup for AI agent workflows

> Configure a Playwright browser proxy, isolate AI agent sessions, verify exit IP continuity, handle retries, and record task results.

Configure a proxy for an authorized agent's network tools when the task needs a regional exit, a continuous browser session, or explicit outbound policy. A text-only task without network tools does not need a proxy. This tutorial runs a three-step browser task: read the exit IP, collect the target page's title and H1, and read the exit IP again.

## Choose an integration and session strategy

HTTP clients can configure HTTP, HTTPS, or SOCKS proxies directly. Browser tasks can set a proxy when Playwright launches the browser. Managed APIs, remote browsers, and MCP are integration patterns that other services may provide; follow the selected service's interface. These patterns do not establish corresponding MiyaIP product features.

| Integration | Session control and configuration you still own |
| - | - |
| Client proxy settings | Select a route through credentials, a port, or the provider's session key; manage client lifetime, retries, parsing, rate limits, and monitoring. |
| Managed scraping API | Use the API's request or session identifier; confirm rendering behavior, target policy, data rights, and result quality. |
| Remote browser | Isolate browser instances or remote contexts; define cookies, task boundaries, target allowlists, and resource cleanup. |
| MCP tool | Follow the MCP server and downstream service's session model; configure tool permissions, input validation, destination policy, secrets, and audit logs. |

For public APIs, datasets, or tests, evaluate a data center exit first. Static ISP exits can suit long-lived regional sessions. Choose residential or mobile exits only when the workflow requires end-user or cellular network characteristics. Select by workflow requirements rather than IP pool size.

| Workflow | Session strategy |
| - | - |
| Stateful tasks such as login or multi-step forms | Use [static residential proxies](/proxy/static-residential/overview) or sticky sessions available in your selected product; keep the same proxy session and browser context throughout the task. |
| Independent, stateless tasks | Refer to [dynamic residential proxies](/proxy/dynamic-residential/overview) for rotation between tasks, and create a new session for a new task. |
| Resuming an authorized task | Store the session reference and encrypted browser state outside prompts, with matching expiration times. |

Select routes in the orchestration layer using stable tenant, workflow, and session identifiers. Keep the exit unchanged during a stateful task, and isolate cookies between customers. The browser context preserves local state; the upstream proxy's fixed or sticky configuration determines exit continuity. This example compares the actual exits before and after the task.

## Install and run the Playwright example

<Steps>
  <Step title="Install the runtime dependencies">
    Install the Node.js project dependency and Chromium in a new working directory.

    ```powershell theme={null}
    npm init -y
    npm install playwright
    npx playwright install chromium
    ```

    Download [ai\_agents.mjs](/examples/ai_agents.mjs) and [retry\_after.mjs](/examples/retry_after.mjs), and save them in the same directory. The example uses the Chromium installed by Playwright.
  </Step>

  <Step title="Configure the proxy, target, and allowlist">
    Replace these placeholders with your extracted proxy details. This example uses an authenticated HTTP or HTTPS proxy and sets the server address separately from credentials.

    ```powershell theme={null}
    $env:PROXY_SERVER = 'http://proxy.example.com:1234'
    $env:PROXY_USERNAME = 'YOUR_USERNAME'
    $env:PROXY_PASSWORD = 'YOUR_PASSWORD'
    $env:TARGET_URL = 'https://example.com/'
    $env:ALLOWED_HOSTS = 'example.com'
    ```

    Inject real credentials through your secret-management tool. Update `ALLOWED_HOSTS` when changing the target; separate multiple approved hosts with commas. The script also permits `api.ipify.org` for exit checks before and after the task. Targets and browser requests must use approved HTTPS hosts on the default port. Keep proxy credentials out of prompts and logs.
  </Step>

  <Step title="Save the complete code">
    One browser launch uses one proxy route, and one context carries the logical session. The script reuses one page for all three steps, blocks network requests outside the allowlist, and closes the context and browser after completion or failure.

    <CodeGroup>
      ```javascript ai_agents.mjs theme={null}
      import { mkdir, writeFile } from "node:fs/promises";
      import { isIP } from "node:net";
      import { pathToFileURL } from "node:url";
      import { RetryAfterOutOfRange, RetryDeferred, retryDelayMs, waitBeforeRetry } from "./retry_after.mjs";

      export const MAX_ATTEMPTS = 2;
      export const MAX_WAIT_MS = 10_000;
      const TRANSIENT_STATUSES = new Set([429, 500, 502, 503, 504]);

      class TaskFailed extends Error {
        constructor(reason) { super(reason); this.reason = reason; }
      }

      function redactIp(value) {
        if (isIP(value) === 4) return value.replace(/\.\d+$/, ".xxx");
        if (isIP(value) === 6) return `${value.split(":").slice(0, 4).join(":")}::`;
        throw new TaskFailed("invalid_exit_ip");
      }

      export async function navigate(page, url, state) {
        for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
          let response;
          try {
            response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
          } catch (error) {
            if (error.name !== "TimeoutError") throw new TaskFailed("navigation_failed");
            if (attempt === MAX_ATTEMPTS - 1) throw new TaskFailed("timeout_attempts_exhausted");
            await waitBeforeRetry(2 ** attempt * 1_000, state.budget,
              (delay) => page.waitForTimeout(delay));
            state.retries += 1;
            continue;
          }
          state.status = response?.status() ?? 0;
          if (state.status === 407) throw new TaskFailed("proxy_authentication_failed");
          if (TRANSIENT_STATUSES.has(state.status)) {
            const delay = retryDelayMs(response.headers()["retry-after"], 2 ** attempt * 1_000);
            if (attempt === MAX_ATTEMPTS - 1) throw new RetryDeferred(delay, "maximum_attempts_reached");
            await waitBeforeRetry(delay, state.budget, (ms) => page.waitForTimeout(ms));
            state.retries += 1;
            continue;
          }
          if (state.status < 200 || state.status >= 400) {
            throw new TaskFailed(`http_status_${state.status}`);
          }
          return response;
        }
        throw new TaskFailed("maximum_attempts_reached");
      }

      export async function runAgent({ chromium, env = process.env, log = (record) => console.log(JSON.stringify(record)) } = {}) {
        const startedAt = performance.now();
        const state = { status: 0, retries: 0, steps: 0, budget: { remainingMs: MAX_WAIT_MS } };
        let browser;
        let context;
        let exitCode = 1;
        try {
          for (const name of ["PROXY_SERVER", "PROXY_USERNAME", "PROXY_PASSWORD", "TARGET_URL"]) {
            if (!env[name]) throw new TaskFailed(`missing_${name}`);
          }
          const proxy = new URL(env.PROXY_SERVER);
          if (!["http:", "https:"].includes(proxy.protocol) || proxy.username || proxy.password) {
            throw new TaskFailed("HTTP_proxy_and_separate_credentials_required");
          }
          const target = new URL(env.TARGET_URL);
          const allowedHosts = new Set((env.ALLOWED_HOSTS || "example.com").split(","));
          allowedHosts.add("api.ipify.org");
          const allowed = (url) => url.protocol === "https:" && allowedHosts.has(url.hostname)
            && ["", "443"].includes(url.port) && !url.username && !url.password;
          if (!allowed(target)) throw new TaskFailed("target_not_allowlisted");
          chromium ??= (await import("playwright")).chromium;
          browser = await chromium.launch({ headless: true, timeout: 30_000, proxy: {
            server: env.PROXY_SERVER, username: env.PROXY_USERNAME, password: env.PROXY_PASSWORD,
          } });
          context = await browser.newContext({ serviceWorkers: "block" });
          await context.route("**/*", async (route) => {
            let permitted = false;
            try { permitted = allowed(new URL(route.request().url())); } catch {}
            if (permitted) await route.continue(); else await route.abort();
          });
          const page = await context.newPage();
          page.setDefaultTimeout(15_000);
          const readExitIp = async () => {
            const response = await navigate(page, "https://api.ipify.org?format=json", state);
            const value = String((await response.json()).ip ?? "");
            redactIp(value);
            state.steps += 1;
            return value;
          };
          const firstExitIp = await readExitIp();
          const targetResponse = await navigate(page, target.href, state);
          const targetStatus = targetResponse.status();
          const result = {
            title: await page.title(),
            heading: (await page.locator("h1").first().textContent() ?? "").trim(),
          };
          if (!result.title || !result.heading) throw new TaskFailed("required_fields_missing");
          result.titleMatchesHeading = result.title === result.heading;
          state.steps += 1;
          const secondExitIp = await readExitIp();
          const sameExitAcrossTask = firstExitIp === secondExitIp;
          const outputDir = env.OUTPUT_DIR || "output";
          await mkdir(outputDir, { recursive: true });
          await writeFile(`${outputDir}/agent-result.json`, JSON.stringify(result, null, 2), "utf8");
          log({ outcome: sameExitAcrossTask ? "success" : "failed", status: targetStatus,
            steps: state.steps, retries: state.retries,
            durationMs: Math.round(performance.now() - startedAt), exitIp: redactIp(firstExitIp),
            sameExitAcrossTask, titleMatchesHeading: result.titleMatchesHeading,
            resultPath: `${outputDir}/agent-result.json` });
          exitCode = sameExitAcrossTask ? 0 : 1;
        } catch (error) {
          const deferred = error instanceof RetryDeferred;
          const reason = error instanceof TaskFailed ? error.reason
            : error instanceof RetryAfterOutOfRange ? "retry_after_out_of_range" : "task_failed";
          log({ outcome: deferred ? "deferred" : "failed", status: state.status,
            steps: state.steps, retries: state.retries,
            durationMs: Math.round(performance.now() - startedAt),
            ...(deferred ? { retry_at: error.retryAt, reason: error.reason } : { reason }) });
          exitCode = deferred ? 2 : 1;
        } finally {
          await context?.close().catch(() => {});
          await browser?.close().catch(() => {});
        }
        return exitCode;
      }

      if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
        process.exitCode = await runAgent();
      }
      ```

      ```javascript retry_after.mjs theme={null}
      export class RetryAfterOutOfRange extends Error {
        constructor() { super("manual_rescheduling_required"); }
      }

      const HTTP_DATE_PATTERNS = [
        /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-9]{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([0-9]{2}):([0-9]{2}):([0-9]{2}) GMT$/,
        /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), ([0-9]{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2}) GMT$/,
        /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( [0-9]|[0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2}) ([0-9]{4})$/,
      ];

      export function retryDelayMs(value, fallbackMs, nowMs = Date.now()) {
        const text = String(value ?? "").trim();
        if (/^[0-9]+$/.test(text)) {
          const seconds = Number(text);
          const maxSeconds = Math.floor((8_640_000_000_000_000 - nowMs) / 1_000);
          if (!Number.isSafeInteger(seconds) || seconds > maxSeconds) {
            throw new RetryAfterOutOfRange();
          }
          return seconds * 1_000;
        }
        const matches = HTTP_DATE_PATTERNS.map((pattern) => pattern.exec(text));
        const format = matches.findIndex(Boolean);
        if (format < 0) return fallbackMs;
        const retryAtMs = Date.parse(text.endsWith(" GMT") ? text : `${text} GMT`);
        if (!Number.isFinite(retryAtMs)) return fallbackMs;
        const fields = matches[format].slice(1);
        const [day, month, year, hour, minute, second] = format === 2
          ? [fields[1], fields[0], fields[5], ...fields.slice(2, 5)] : fields;
        const date = new Date(retryAtMs);
        const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        const actualYear = format === 1 ? date.getUTCFullYear() % 100 : date.getUTCFullYear();
        // Date.parse can normalize impossible dates such as 31 February.
        if (date.getUTCDate() !== Number(day) || months[date.getUTCMonth()] !== month
          || actualYear !== Number(year) || date.getUTCHours() !== Number(hour)
          || date.getUTCMinutes() !== Number(minute) || date.getUTCSeconds() !== Number(second)) {
          return fallbackMs;
        }
        return Math.max(Math.ceil((retryAtMs - nowMs) / 1_000) * 1_000, 0);
      }

      export class RetryDeferred extends Error {
        constructor(delayMs, reason, nowMs = Date.now()) {
          super("retry_deferred");
          const retryAt = new Date(nowMs + delayMs);
          if (!Number.isFinite(retryAt.getTime())) throw new RetryAfterOutOfRange();
          this.retryAt = retryAt.toISOString();
          this.reason = reason;
        }
      }

      export async function waitBeforeRetry(delayMs, budget, sleep) {
        if (delayMs > budget.remainingMs) {
          throw new RetryDeferred(delayMs, "waiting_budget_exhausted");
        }
        if (delayMs > 0) await sleep(delayMs);
        budget.remainingMs -= delayMs;
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Run and inspect this task's results">
    ```powershell theme={null}
    node ai_agents.mjs
    node --version
    npm list playwright
    ```

    Inspect the console's `outcome`, target `status`, `steps`, `retries`, `durationMs`, masked `exitIp`, and `sameExitAcrossTask`. The current run's `title`, `heading`, and `titleMatchesHeading` are saved to `output/agent-result.json`. Different title and H1 values do not by themselves indicate a network failure; apply the target's data rules.

    The exit code is `0` when all three steps finish and the exit remains the same, `1` for an exit change or other failure, and `2` when the task must be deferred. No successful result is prefilled; use your actual run's output.
  </Step>
</Steps>

## Retry and resume tasks

Each navigation makes at most 2 attempts. Navigation, default page-operation, and browser-launch timeouts are 30, 15, and 30 seconds. The cumulative retry waiting budget is 10 seconds per task. Valid `Retry-After` seconds or HTTP dates for 429, 500, 502, 503, and 504 are preserved in full; they **are not shortened to 10 seconds**. Only missing or invalid headers use bounded exponential backoff.

If the delay exceeds the remaining budget, or the final attempt still returns a transient error, the script reports `outcome=deferred`, a UTC `retry_at`, and a reason, then closes the browser and exits. Resume at or after `retry_at` in your orchestration layer; the example does not create a schedule. Dates outside the runtime's supported range stop execution with `retry_after_out_of_range` and require manual rescheduling.

| Result | Action |
| - | - |
| 407, proxy connection, or authentication failure | Stop and check connection fields instead of rotating IPs to retry invalid credentials. |
| 429 or transient server error | Honor the server's requested time, and defer the task when the wait exceeds the budget. |
| Timeout | Retry within the attempt and waiting limits, then stop when attempts are exhausted. |
| Deterministic 4xx or policy rejection | Fix the request or authorization without retrying. |
| Browser or parsing error | Record a masked error category and failed step; restart a context only when its state can be rebuilt safely. |
| Target outside the allowlist or missing fields | Stop and check task configuration, page structure, and selectors. |
| Different exit before and after the task | Record failure and check fixed or sticky session configuration. |

## Connect the agent's orchestration layer

Use approved HTTP or browser tools for network requests. The orchestration layer validates the destination, tenant, task, and route key before a gateway selects an upstream exit. Inject credentials only at runtime; give the model the required results and masked status.

Production infrastructure also needs network controls for DNS rebinding, private and metadata addresses, short-lived credentials, concurrency, and bandwidth. Preserve idempotency keys for writes, configure circuit breakers based on exit health, and isolate tenants and browser storage. Configure these controls in your infrastructure; the single-file example demonstrates browser requests and session flow.

Log time, task identifiers, hashed session identifiers, hostnames, steps, attempts, status categories, and durations. Exclude proxy passwords, cookies, Authorization headers, raw exceptions, and full URLs containing secret query values. Test authentication failure, rate limits, timeouts, and deferred execution with controlled responses before increasing task volume. For page collection and file export, see [web scraping](/developers/web-scraping).

See [Playwright's proxy documentation](https://playwright.dev/docs/network#http-proxy) for browser configuration and [RFC 9110: Retry-After](https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after) for retry header formats.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.