ScreenshotNeo

BlogHow-to

How to Capture a Screenshot When a Login Session Expires During Page Loading

Capture the expired-session state as evidence, or refresh authentication and capture the intended page. Here’s how to choose the right moment and protect saved login state.

By the ScreenshotNeo team4 October 20269 min read

When a login session expires during page loading, decide what the screenshot is meant to show. If you need evidence of the interruption, capture the browser’s current state immediately. If you need the protected page, renew authentication, revisit the target, and capture only after confirming that the signed-in page is present. A screenshot records the page as it is at capture time; it cannot recover content hidden behind an expired session.

This guide uses Playwright for the browser-based method. The examples use its JavaScript API and Node.js. Keep a failure-state screenshot separate from a later capture of the restored page, and label each with the target URL and where it occurred in the flow.

1. Choose what the screenshot needs to prove

Goal What to capture When to capture
Document the failure The sign-in redirect, expired-session notice, or partially loaded page As soon as the failure state appears
Capture protected content The intended authenticated page After refreshing authentication, revisiting the target, and confirming a signed-in signal

These are different artifacts. A screenshot of a redirect is useful evidence of an authentication problem, but it is not evidence of what the protected page looked like. If both matter, save both captures with distinct filenames.

2. Capture the expired-session state with Playwright

For failure evidence, navigate to the target, detect a known expired-session signal, and take the screenshot before changing authentication or retrying. The example below checks either a sign-in URL or a visible message. Replace the URL and selectors with signals specific to your application.

import { chromium } from 'playwright';

const targetUrl = 'https://example.com/account/report';
const signInPath = '/sign-in';
const expiredMessage = 'text=Your session has expired';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

try {
  // domcontentloaded allows capture before images and other load work finish.
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });

  const redirectedToSignIn = new URL(page.url()).pathname.startsWith(signInPath);
  const showsExpiredMessage = await page.locator(expiredMessage).isVisible().catch(() => false);

  if (redirectedToSignIn || showsExpiredMessage) {
    await page.screenshot({ path: 'session-expired.png', fullPage: true });
    console.log(`Captured failure state at ${page.url()}`);
  } else {
    console.log(`No configured expiration signal found; current URL: ${page.url()}`);
    // Capture only if a partial-loading snapshot is useful for this investigation.
    await page.screenshot({ path: 'page-current-state.png', fullPage: true });
  }
} finally {
  await browser.close();
}

Install Playwright and its browser in your project before running the script. This is a complete script once the package and browser are available:

npm install playwright
npx playwright install chromium
node capture-expired.mjs

Pick the navigation milestone that matches the evidence

page.goto() can wait for different navigation milestones:

  • commit: the response has started arriving and the document navigation has committed. Use it when the earliest browser-visible response is what you want to inspect.
  • domcontentloaded: the HTML has been parsed and the DOMContentLoaded event fired. This is often a useful point for capturing a redirect or a page that is still loading resources.
  • load: the load event fired after page resources finished loading. Use it when the fully loaded document is the target.
  • networkidle: Playwright documents this state, but discourages relying on it for tests. Applications with persistent requests may never become idle; prefer a page-specific condition.

For example, change the navigation wait to { waitUntil: 'commit' } for an earlier snapshot, or { waitUntil: 'load' } when you need the load event. The appropriate point depends on whether you are recording an early response, parsed page, or completed load. A browser may not have painted all visible content at the earliest milestone, so inspect the resulting evidence in context.

3. Refresh authentication and capture the intended page

If the protected page is the goal, use the site’s supported sign-in process to refresh the state. Playwright’s authentication guidance supports saving and reusing authenticated browser state, but that state can expire. Once it does, renew it and verify the signed-in page before capturing the target.

A robust workflow has two parts: an authentication setup that produces fresh state, and a capture script that loads that state and checks the expected page. The setup below is intentionally application-specific: complete the login through your approved test account flow, then save the context state after the signed-in page is verified.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();

try {
  await page.goto('https://example.com/sign-in', { waitUntil: 'domcontentloaded' });

  // Replace these steps with the application's supported authentication flow.
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Verify a page-specific signal that only appears when signed in.
  await page.getByRole('navigation', { name: 'Account' }).waitFor({ state: 'visible', timeout: 15_000 });
  await context.storageState({ path: 'playwright/.auth/user.json' });
} finally {
  await browser.close();
}

Then load that state, revisit the intended URL, and wait for a meaningful signal before taking the screenshot:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const page = await context.newPage();

try {
  await page.goto('https://example.com/account/report', {
    waitUntil: 'domcontentloaded',
    timeout: 30_000
  });

  // Use an expected URL, heading, or signed-in control from your application.
  await page.getByRole('heading', { name: 'Monthly report' }).waitFor({
    state: 'visible',
    timeout: 15_000
  });

  await page.screenshot({ path: 'authenticated-report.png', fullPage: true });
} finally {
  await browser.close();
}

Adapt the selectors and authentication steps to the application. Some sites use single sign-on, multifactor authentication, or an approved test identity provider; use the supported test flow rather than bypassing those controls. If the expected signal does not appear, stop and capture or report the failure state instead of labeling it as a successful authenticated-page screenshot.

Keep authentication state private

Saved browser state can contain cookies and headers that let someone act as the account. Store it as a secret, restrict access, and exclude the authentication directory from source control. Do not attach the state file to a bug report or share it as if it were an ordinary screenshot. Refresh it when authentication expires, then verify the signed-in state again.

4. Wait for the state you need, not an arbitrary delay

A fixed sleep can be too short on a slow run and waste time on a fast one. Prefer a condition tied to the page:

  • For a failure capture, check for the sign-in URL or expiration message.
  • For a successful capture, check for the target page’s heading, account navigation, or another signed-in-only element.
  • When redirects are involved, assert the resulting URL or wait for the expected URL.
  • When the page is intentionally incomplete evidence, choose an early navigation milestone and capture that state directly.

Use a timeout as a bound on how long to wait for the condition, not as proof that the page is ready. If the condition times out, record the current URL and capture the failure only if it helps diagnose the problem.

5. Capture options and evidence handling

Playwright’s page.screenshot() supports screenshot options such as a file path and full-page capture. Choose the smallest capture that preserves the evidence you need:

  • Use path to save a reproducible artifact with a clear name.
  • Use fullPage: true when the relevant message or content may be below the viewport. For a loading failure, a viewport capture may better preserve what was visible at the interruption.
  • Capture the failure and the recovered page into separate files. Include the target URL, timestamp, and flow stage in the bug report or artifact metadata.
  • Do not put passwords, access tokens, or session cookies in filenames, logs, or issue descriptions.

6. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It can capture a URL in one GET request and return an image or PDF. Use it when the target page is publicly reachable and you want a hosted browser capture without maintaining Playwright setup. It cannot restore a private login session from this URL-only example; for authenticated content, configure an appropriate supported request authentication method or use the browser-state workflow above.

API documentation: ScreenshotNeo API docs.

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

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

Replace YOUR_API_KEY with your key and the example URL with the page to capture. The JavaScript example uses Bun’s Bun.write to save the response; in Node.js, use this runnable equivalent:

import { writeFile } from 'node:fs/promises';

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month.

7. Troubleshooting

Symptom Likely cause What to do
The screenshot shows the sign-in page, not the protected page The session expired, or the saved state was stale Refresh authentication using the supported flow, save fresh state, revisit the target, and verify a signed-in-only signal before capture.
The script captures too early The selected navigation milestone occurs before the needed content is ready Wait for a page-specific heading or visible element. Use load only if full resource loading is relevant.
The script waits indefinitely or hits a timeout The app has persistent network activity, a slow response, or a condition that never appears Avoid using network idle as the sole readiness check. Confirm the selector and URL for the current state, then set a reasonable condition timeout.
The expired-session check misses the redirect The site uses a different sign-in path, client-side message, or locale-specific text Inspect the actual redirected URL and DOM. Update the URL predicate or selector to match the app’s stable signal.
The failure screenshot is blank or incomplete The capture happened at a very early milestone or the page had not painted useful content Try domcontentloaded or wait for the specific error element, depending on what the evidence should show.
Authentication works locally but not in automation The stored state expired, the test account flow changed, or state was saved before sign-in completed Rerun the authentication setup and verify the signed-in marker before saving state.
A screenshot or log exposes account access Authentication state or secret values were stored or shared unsafely Restrict and remove access to the state artifact, keep it out of source control and reports, and rotate credentials or sessions if exposed.

8. Performance, reliability, and cost

For the Playwright method, the main time cost is browser startup, navigation, and waiting for the chosen readiness condition. Reuse a browser process for multiple captures in a controlled job when practical, but use a fresh context when you need isolation between accounts or capture runs. Do not make a fixed long sleep the default; a specific signal usually makes runs both faster and more reliable.

Authentication is the main reliability boundary. Sessions expire, login screens change, and a successful navigation does not prove that the protected content loaded. Make the signed-in signal an explicit precondition for a success capture. If it is absent, preserve the failure state separately and mark the intended capture as unsuccessful.

Playwright itself is browser automation software; infrastructure and execution cost depend on where and how it runs. ScreenshotNeo offers 1,000 shots per month free without a card; paid options are Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. For this particular expired-session problem, a hosted URL capture does not replace refreshing protected browser authentication.

9. Frequently asked questions

Can I capture the page before it finishes loading?

Yes. Navigate with an earlier milestone such as commit or domcontentloaded and capture the current state. Choose the milestone based on what you need to show, and remember that very early captures may not have painted useful content.

Should I wait for network idle?

Usually, use a page-specific condition instead. Playwright discourages network idle as a test readiness strategy, and persistent requests can prevent it from occurring.

Does reusing authentication state make the session permanent?

No. Reusable state helps start a browser signed in, but it can expire. Refresh it through the supported authentication flow and verify the protected page before capture.

Can I use the failure screenshot as proof of the protected content?

No. It proves what the browser displayed at that point in the flow. Capture the protected page separately after authentication is restored.