How to Get the HTML Content of a Puppeteer Frame
Use Puppeteer's Frame.content() to read a frame's complete HTML, including its DOCTYPE. Learn how to find the right iframe, handle dynamic content, and troubleshoot common issues.
Use Puppeteer’s Frame.content() method to get a frame’s complete HTML as a string, including its DOCTYPE:
const html = await frame.content();
If you have an iframe element handle rather than a Frame, call contentFrame() first, then read the returned frame. page.content() reads the main page document; it does not select an embedded frame for you.
Get the HTML from an iframe element
This runnable Node.js example launches Chromium, opens a page, finds an iframe, resolves its associated frame, and prints the full frame HTML. Replace the URL and selector with those for your page.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com');
const iframeHandle = await page.$('iframe#report');
if (!iframeHandle) {
throw new Error('iframe#report was not found');
}
const frame = await iframeHandle.contentFrame();
if (!frame) {
throw new Error('Could not resolve the iframe frame');
}
const html = await frame.content();
console.log(html);
} finally {
await browser.close();
}
The example uses top-level await, which works in an ES module. Save it as an .mjs file or use a project configured with "type": "module". Install Puppeteer in that project with npm install puppeteer; Puppeteer downloads a compatible browser as part of its normal installation.
Choose the frame you need
A page can contain multiple frames, including nested frames. Use a selector or a rule based on the page’s frame tree and URLs to choose the content you actually need. The appropriate selector is specific to the site.
// List frames attached to the page.
for (const frame of page.frames()) {
console.log({ url: frame.url(), name: frame.name() });
}
// Resolve the frame associated with a known iframe element.
const iframeHandle = await page.$('iframe#report');
if (!iframeHandle) throw new Error('iframe#report was not found');
const frame = await iframeHandle.contentFrame();
if (!frame) throw new Error('Could not resolve the iframe frame');
const html = await frame.content();
ElementHandle.contentFrame() resolves the frame associated with an iframe element. Check that the selector matched before calling it. If you already have the target Frame, skip the handle lookup and call frame.content() directly.
Wait for dynamic frame content
frame.content() reads the frame’s current document. A page may create or populate the iframe after navigation, so wait for a condition that reflects the content you need before reading it. There is no single readiness condition that fits every site.
// Wait until a page-specific element exists inside the frame.
await frame.waitForSelector('.report-ready');
const html = await frame.content();
Use a selector that signals the desired content is ready, rather than assuming the iframe is complete as soon as the main page’s navigation finishes. If the iframe itself is added later, first wait for its element on the page, resolve its frame, then wait for the inner content.
Pick the right HTML-reading method
| Need | Method | What it returns |
|---|---|---|
| Complete frame document | frame.content() |
HTML string for the frame, including DOCTYPE. |
| Complete main page document | page.content() |
HTML string for the page’s main document, including DOCTYPE. |
| A DOM-derived value in the frame | frame.evaluate(() => document.documentElement.outerHTML) |
The result of evaluating that expression in the frame context. |
| A particular matching element | frame.$eval(selector, fn) |
The function result for the first matching element; it throws if no element matches. |
For the entire frame document, frame.content() is the direct API. Use evaluate() when you specifically need a DOM expression’s result, or $eval() when you only need a particular element.
Or skip the browser setup
If your goal is to capture how a page looks rather than inspect a frame’s source HTML, ScreenshotNeo provides a one-request screenshot API. It captures screenshots and PDFs; it does not return a Puppeteer frame’s HTML.
For example, save a screenshot of a URL with cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request parameters. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use its screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month, with no card.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
iframeHandle is null |
The selector did not match an iframe at lookup time, or the iframe has not been added yet. | Check the selector and wait for the iframe element before querying it. |
frame is null |
The handle did not resolve to an available frame. | Confirm the matched element is the intended iframe and that it is attached; resolve it again after the page updates. |
| The returned HTML is incomplete or lacks expected content | The frame’s content is populated dynamically after it exists. | Wait for a page-specific selector or other content-ready condition inside the frame, then call content(). |
| You got the outer page’s HTML | page.content() was called instead of selecting the child frame. |
Resolve the iframe with contentFrame() and call frame.content(). |
$eval() reports no matching element |
The selector does not match an element in that frame, or the element is not ready. | Check the selector and wait for the element inside the frame. For the entire document, use frame.content(). |
Performance and reliability notes
content()returns the frame document as one string, so large documents can require substantial memory when retained or logged. Extract only the fields you need if full markup is unnecessary.- Read after the frame is attached and the required content is ready. A successful main-page navigation does not by itself establish that an embedded application’s data has loaded.
- Wrap browser use in
try/finallyand close it when done, as in the example, so errors during extraction do not leave the browser running. - The Puppeteer approach requires a browser process and page setup. For repeated jobs, reuse a managed browser where appropriate and avoid launching one for every individual extraction.
FAQ
Does frame.content() include the DOCTYPE?
Yes. It returns the frame’s full HTML contents, including its DOCTYPE.
Can I get HTML from a cross-origin iframe?
Use Puppeteer’s frame APIs to select the iframe’s associated Frame and read its content. Do not substitute page-level DOM access for selecting the frame.
Does content() return the original server response?
It returns the frame’s current HTML document, which reflects the document state at the time you call it. It is not a promise of the original response bytes.


