ScreenshotNeo

BlogHow-to

How to Intentionally Fail Screenshot API Requests

Test HTTP 500s, 503s, network failures, provider errors, retries, and screenshot error states with Playwright and hosted APIs.

By the ScreenshotNeo team1 October 20267 min read

To intentionally fail a screenshot API request, inject the failure at the layer you want to test:

  • Return 500 or 503 with route.fulfill() to test an HTTP error response.
  • Use route.abort() or offline mode to test a transport-level network failure.
  • Fail a required subresource to test incomplete page data.
  • Send invalid credentials, malformed input, or controlled quota usage to test provider-side errors.

These cases are different. A browser can receive a 503 response successfully; a transport failure means it never receives an HTTP response. Test the branch your application actually uses.

1. Decide which failure you need

Failure Injection What the client should observe
Application/server error Fulfill the route with status 500 or 503 Error UI renders, loading ends, retry behavior is correct
Transport failure Abort the route or enable offline mode Network-error path renders; the client does not claim success
Failed subresource Abort a required API, image, or script request Capture fails or the page reports missing critical data
Authentication or validation Use malformed input or controlled invalid credentials Documented 400/401 handling without secret leakage
Rate limit Use a safe test quota or provider sandbox Backoff and user messaging follow the contract

Playwright documents both HTTP interception and route modification in its network mocking guide. Its Page API also explains that HTTP errors such as 404 and 503 are still successful responses from the HTTP standpoint; a request is failed when the client cannot obtain an HTTP response.

2. Mock an HTTP 503 with Playwright

Use this when your application should receive a normal HTTP response containing an error status.

import { test, expect } from '@playwright/test';

test('shows an error state for a screenshot API 503', async ({ page }) => {
  await page.route('**/api/screenshot**', async (route) => {
    await route.fulfill({
      status: 503,
      contentType: 'application/json',
      body: JSON.stringify({
        error: 'screenshot_service_unavailable'
      })
    });
  });

  await page.goto('http://localhost:3000/capture');
  await page.getByRole('button', { name: 'Capture screenshot' }).click();

  await expect(page.getByRole('alert')).toContainText('temporarily unavailable');
  await expect(page.getByRole('progressbar')).toBeHidden();

  await page.unroute('**/api/screenshot**');
});

Change the URL pattern to match the request your application actually makes. Keep the response body and headers close to production so the test exercises parsing, logging, and retry behavior too.

Return a 500 instead

await page.route('**/api/screenshot**', async (route) => {
  await route.fulfill({
    status: 500,
    contentType: 'application/json',
    body: JSON.stringify({ error: 'internal_error' })
  });
});

3. Test retry and recovery

A useful failure test removes the mock and retries the same action. This verifies that the UI leaves the error state and does not keep an old failure cached.

import { test, expect } from '@playwright/test';

test('recovers after a temporary screenshot API failure', async ({ page }) => {
  let attempts = 0;

  await page.route('**/api/screenshot**', async (route) => {
    attempts += 1;
    if (attempts === 1) {
      await route.fulfill({
        status: 503,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'temporary_failure' })
      });
      return;
    }
    await route.continue();
  });

  await page.goto('http://localhost:3000/capture');
  await page.getByRole('button', { name: 'Capture screenshot' }).click();
  await expect(page.getByRole('alert')).toBeVisible();

  await page.getByRole('button', { name: 'Retry' }).click();
  await expect(page.getByRole('img', { name: 'Screenshot result' })).toBeVisible();
});

Assert the visible contract: the spinner stops, the message is truthful, the retry is available when appropriate, and a successful response replaces the error.

4. Simulate a transport-level failure

Use route.abort() when you want the browser to receive no HTTP response. This exercises a different code path from a 503.

import { test, expect } from '@playwright/test';

test('shows a network error when the screenshot request is aborted', async ({ page }) => {
  await page.route('**/api/screenshot**', async (route) => {
    await route.abort('failed');
  });

  await page.goto('http://localhost:3000/capture');
  await page.getByRole('button', { name: 'Capture screenshot' }).click();

  await expect(page.getByRole('alert')).toContainText('network');
  await expect(page.getByRole('progressbar')).toBeHidden();
});

You can also test broad connectivity handling with a browser context configured offline:

import { test, expect } from '@playwright/test';

test('handles offline mode', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('http://localhost:3000/capture');
  await context.setOffline(true);

  await page.getByRole('button', { name: 'Capture screenshot' }).click();
  await expect(page.getByRole('alert')).toContainText('network');

  await context.setOffline(false);
  await context.close();
});

5. Fail a required subresource

Pages often load data through a separate request. Abort that request to verify that a missing API response, image, or script cannot produce a false-success screenshot.

await page.route('**/api/report-data**', async (route) => {
  await route.abort('failed');
});

await page.goto('http://localhost:3000/report');
await expect(page.getByRole('alert')).toContainText('could not load report');

If the resource is optional, the expected result may be a successful capture with a placeholder. Make that distinction explicit in the test.

6. Configure hosted screenshot APIs to fail

Some hosted providers expose provider-side failure controls. ScreenshotOne documents fail_if_request_failed: for a matching resource URL, rendering fails when the resource has a browser or network error or returns an HTTP status from 400 through 599. Use a narrow URL pattern so an unrelated asset does not invalidate the entire capture.

ApiFlash documents fail_on_status, which accepts comma-separated statuses or hyphen-separated ranges such as 400,404,500-511. Treat these controls as vendor-specific and verify the current provider documentation before relying on them in a contract test.

7. Test provider validation, authentication, and rate limits

  1. Use a dedicated test key or sandbox where available.
  2. Send malformed input to exercise a 400 response.
  3. Omit or deliberately alter credentials to exercise a 401 response.
  4. Use a safe, bounded quota test for a 429 response; never exhaust a production account accidentally.
  5. Assert that secrets are absent from rendered error messages, screenshots, logs, and telemetry.

Provider status codes and render errors are vendor-specific. Keep assertions focused on your client contract: status classification, user message, retry policy, and redaction.

8. Capture the failure state itself

If the purpose of the test is visual regression, take the screenshot after the error UI is stable.

await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('progressbar')).toBeHidden();
await page.screenshot({
  path: 'artifacts/screenshot-api-503.png',
  fullPage: true
});

Wait for the specific alert or stable selector instead of using an arbitrary delay. This reduces flaky captures and proves that the page reached the intended state.

9. Troubleshooting

The mock never runs

Cause: The route pattern does not match the real URL, method, or query string, or the request happened before the route was registered.

Fix: Register the route before navigation or the action that triggers the request. Temporarily log the request URL and use a broader pattern, then narrow it after the test passes.

The test expects requestfailed for a 503

Cause: A 503 is an HTTP response. The browser received it successfully.

Fix: Assert the response status or application error state for 503. Use route.abort() or offline mode for a transport failure.

The page stays on an endless spinner

Cause: The application has no terminal state for the injected failure, or the mock body does not match the parser’s expected schema.

Fix: Return the same content type and JSON shape used by the real provider, then assert that loading ends for both HTTP and network failures.

Retry keeps failing

Cause: The route remains mocked, or the retry reuses a cached failed response.

Fix: Call page.unroute(), allow the next request through, and clear or bypass the relevant cache in the test environment.

A resource failure does not fail the render

Cause: The provider treats failed subresources as non-fatal by default.

Fix: Enable the provider’s documented fail-on-resource option, such as ScreenshotOne’s fail_if_request_failed, and match only the required resource.

10. Reliability, performance, and cost notes

  • Prefer deterministic route interception over real outages. It is faster, repeatable, and safe for CI.
  • Keep one test for each semantic class: HTTP error, transport failure, subresource failure, authentication, validation, and rate limit.
  • Use short, explicit timeouts for failure tests, but allow enough time for the browser to receive the mocked response.
  • Do not retry every error automatically. Retry transient 5xx and network failures according to your product contract; show validation and authentication errors clearly.
  • Record status, provider request ID, retry count, and elapsed time without recording credentials or private page data.
  • When testing a paid hosted API, use mocks for most CI runs and a small scheduled contract suite for provider behavior. Check current quotas and pricing before enabling it.

11. Or skip the browser setup

ScreenshotNeo provides a single HTTP request for website screenshots and PDFs. Its clean-shot pipeline accepts cookie and consent banners like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets each step be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; the response identifies the result with X-Page-Verdict and X-Billed headers.

For a normal capture:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', buffer);

See the ScreenshotNeo API documentation for request options. The API supports PNG, JPEG, WebP, PDF, full-page captures with lazy images loaded, CSS-selector element captures, device presets, retina scale, custom CSS and JavaScript, clicks, waits, blocked requests, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, usage data, and an OpenAPI specification.

ScreenshotNeo also exposes an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. It offers 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

12. FAQ

Is a 500 response the same as a failed request?

No. A 500 is an HTTP response. A failed request in the transport sense means no HTTP response was obtained.

Should I test 500 or 503?

Test both if your application treats them differently. Use 500 for an unexpected server error and 503 for temporary unavailability.

How do I prove the retry really worked?

Count attempts, remove the mock or allow the next request through, then assert that the success UI replaces the error state.

Can I use real provider outages in CI?

Use deterministic mocks for routine CI. Reserve real provider checks for a bounded contract test with a safe account or sandbox.