How to Read POST Data from a Request in Puppeteer
Read POST request bodies in Puppeteer with the request event and fetchPostData(). Learn how to filter requests, handle missing data, and avoid interception stalls.
Use Puppeteer’s page.on('request') event to observe page requests. Filter for POST, then await request.fetchPostData() to get the body as a string. Handle undefined, which means the body could not be retrieved. You do not need request interception just to read request data.
page.on('request', async request => {
if (request.method() !== 'POST' || !request.hasPostData()) return;
const body = await request.fetchPostData();
if (body === undefined) {
console.log('POST body unavailable');
return;
}
console.log(request.url(), body);
});
This guide assumes a current Puppeteer version with HTTPRequest.fetchPostData(). See the HTTPRequest API and network logging guide.
1. Set up a runnable Puppeteer example
Install Puppeteer in a new Node.js project:
npm install puppeteer
Save this as read-post-data.js. It starts a small local server that accepts a form POST, opens the page in Puppeteer, submits the form, and prints the observed request URL and body. The listener is attached before submission so it does not miss the request.
const http = require('node:http');
const puppeteer = require('puppeteer');
async function main() {
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/submit') {
let body = '';
req.setEncoding('utf8');
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end('<p>Submitted</p>');
});
return;
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(`<form action="/submit" method="post">
<input name="email" value="dev@example.com">
<input name="topic" value="puppeteer">
<button type="submit">Send</button>
</form>`);
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
const origin = `http://127.0.0.1:${address.port}`;
let browser;
try {
browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
page.on('request', async request => {
if (request.method() !== 'POST' || !request.hasPostData()) return;
try {
const body = await request.fetchPostData();
console.log('POST URL:', request.url());
console.log('POST body:', body === undefined ? '<unavailable>' : body);
} catch (error) {
console.error('Could not fetch POST body:', error.message);
}
});
await page.goto(origin);
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click('button[type="submit"]')
]);
} finally {
if (browser) await browser.close();
await new Promise(resolve => server.close(resolve));
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with node read-post-data.js. The form uses the browser’s default URL-encoded encoding, so the body will look like email=dev%40example.com&topic=puppeteer. The callback parses the request on the server only to demonstrate that the request completes; Puppeteer logs the body it observed in the browser.
2. Choose and filter the request
The request event fires for page-initiated network requests, not only form submissions. A page can issue requests for scripts, images, analytics, background API calls, and navigations. Narrow the match to the request you care about.
| Check | Use | Example |
|---|---|---|
| HTTP method | Only inspect POST requests | request.method() === 'POST' |
| Body presence | Skip requests with no POST data | request.hasPostData() |
| URL | Target one endpoint or path | new URL(request.url()).pathname === '/api/orders' |
| Resource type | Distinguish document, fetch, and other request kinds | request.resourceType() === 'fetch' |
| Headers | Interpret the body using its declared content type | request.headers()['content-type'] |
For example, combine method and endpoint checks:
page.on('request', async request => {
if (request.method() !== 'POST') return;
const url = new URL(request.url());
if (url.pathname !== '/api/orders') return;
const body = await request.fetchPostData();
console.log({ url: request.url(), contentType: request.headers()['content-type'], body });
});
Check the hostname too if the same path could exist on more than one origin. Avoid logging credentials, personal data, or payment details in production logs.
3. Understand the POST body and its limits
fetchPostData() returns Promise<string | undefined>. Await it and branch on undefined. hasPostData() is a useful filter, but it does not guarantee that a decoded body will be available through the deprecated postData() method. Puppeteer documents fetchPostData() as the current method for fetching POST data from the browser.
The API returns a string; do not assume every content type is JSON or form-encoded. Inspect request.headers()['content-type'] and interpret the string according to the endpoint’s encoding. For JSON, parse defensively:
const contentType = request.headers()['content-type'] || '';
const body = await request.fetchPostData();
if (body === undefined) {
console.log('No body available');
} else if (contentType.includes('application/json')) {
try {
console.log(JSON.parse(body));
} catch {
console.log('Body was not valid JSON:', body);
}
} else {
console.log('Raw body:', body);
}
For URL-encoded form data, use URLSearchParams rather than splitting on ampersands manually:
const form = new URLSearchParams(body);
console.log(form.get('email'));
For multipart forms, binary uploads, or unusually large payloads, the returned string may not be a convenient representation for application-level parsing. The official API describes a string-or-undefined result; it does not promise a single serialization format for every content type. Treat the body as sensitive and avoid retaining it longer than needed.
4. Observe requests without interception
For read-only logging, page.on('request', handler) is enough. Request interception serves a different purpose: modifying, fulfilling, or aborting requests. Enabling it makes requests stall until they are resolved, unless completed by browser cache. Adding interception solely to read a POST body creates extra ways to leave a page waiting.
Keep event callbacks bounded and catch asynchronous errors. Event emitters do not automatically wait for an async listener to finish before continuing page activity. A production collector can make failures visible without allowing one failed read to become an unhandled rejection:
page.on('request', request => {
if (request.method() !== 'POST') return;
void (async () => {
try {
const body = await request.fetchPostData();
console.log(body === undefined ? 'Body unavailable' : body);
} catch (error) {
console.error('POST body read failed:', error.message);
}
})();
});
If you have many requests, filter before calling fetchPostData() and avoid expensive processing in the event handler. This keeps logging overhead and memory use under control.
5. If interception is already required
Use interception only when you also need to alter, block, or fulfill traffic. Once enabled, every intercepted request needs a resolution path. In addition, another listener or package may resolve a request first. Check isInterceptResolutionHandled() immediately before calling continue(), abort(), or respond(); do not put an await between the check and resolution.
await page.setRequestInterception(true);
page.on('request', async request => {
try {
if (request.method() === 'POST') {
const body = await request.fetchPostData();
console.log('POST body:', body === undefined ? '<unavailable>' : body);
}
} catch (error) {
console.error('Could not inspect request:', error.message);
}
// Recheck after asynchronous work: a different handler may have resolved it.
if (request.isInterceptResolutionHandled()) return;
request.continue();
});
Make sure every branch reaches a resolution call. If your logic conditionally aborts or fulfills a request, those branches must also check the handled state immediately before resolution. The official interception guide explains the resolution behavior and coordination pattern.
6. cURL, Python, and Node.js alternatives
Puppeteer is a Node.js browser automation library, so its request event and HTTPRequest methods are used from JavaScript. cURL and Python can send or inspect HTTP requests directly, but they do not expose Puppeteer’s in-page request event. Use them when you control the client request or need to reproduce an endpoint outside the browser.
Send a comparable POST with cURL
curl -i -X POST 'https://example.com/api/orders' \
-H 'Content-Type: application/json' \
--data '{"item":"book","quantity":1}'
Send a comparable POST with Python
import requests
response = requests.post(
"https://example.com/api/orders",
json={"item": "book", "quantity": 1},
timeout=30,
)
print(response.status_code)
print(response.text)
Send a comparable POST with Node.js fetch
const response = await fetch('https://example.com/api/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ item: 'book', quantity: 1 })
});
console.log(response.status, await response.text());
These examples send a request; they do not capture traffic initiated by a webpage. To observe the browser’s own POST, use the Puppeteer request event above.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
request.postData() returns undefined |
The method is deprecated and may not return data that is long or not readily decoded. | Use await request.fetchPostData() and handle undefined. |
fetchPostData() returns undefined |
The browser could not provide the body for that request. | Keep the undefined case explicit; confirm the request is the intended POST and inspect headers and URL. Do not assume a body is recoverable for every request. |
| No event appears | The listener may be attached after the request, the page may not issue a POST, or the request may not be page initiated. | Attach before navigation or interaction; check the method and endpoint; verify the action actually submits. |
| Many unrelated POSTs appear | The event reports page network requests, including background calls. | Filter by hostname, path, method, and if useful resource type. |
| Navigation hangs after enabling interception | At least one intercepted request was not resolved. | Ensure every path calls continue(), respond(), or abort(), subject to handled-state checks. |
| “Request is already handled!” | Another listener or package resolved the intercepted request first. | Check isInterceptResolutionHandled() immediately before resolution, with no asynchronous gap. |
| Body parsing fails | The body is not the format assumed by the parser, or the content type differs. | Inspect the content type and handle the returned string according to that format; validate JSON before using it. |
8. Performance, reliability, and cost
- Filter early: check method and URL before fetching or parsing bodies.
- Keep work small: avoid large synchronous parsing or network writes inside the event callback.
- Handle asynchronous failures: catch errors from
fetchPostData()so one unavailable body does not create an unhandled rejection. - Bound storage: request bodies can contain secrets or personal information. Redact sensitive fields and avoid unnecessary persistence.
- Resolve interception promptly: when interception is in use, unresolved requests can delay navigation and page work.
- Cost: Puppeteer itself does not charge per request; infrastructure, browser runtime, logging, and storage costs depend on where and how you run the automation. This guide does not assume a benchmark or fixed runtime cost.
9. Or skip the browser setup
If your goal is a clean page image rather than inspecting the POST body, ScreenshotNeo captures a URL through one API request. See the ScreenshotNeo API documentation for its options. It does not expose Puppeteer POST bodies; use Puppeteer when you need to inspect request data.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
With ScreenshotNeo, cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.
10. FAQ
Should I use request.postData() or fetchPostData()?
Use fetchPostData(). Puppeteer marks postData() deprecated and documents the fetch method for retrieving the body.
Does reading a POST body require interception?
No. Listen for the page’s request event. Interception is for controlling requests and requires resolving them.
Can I read a POST body from a request that happened before my listener was attached?
No event listener can observe an event that has already fired. Attach the handler before the navigation or user action that creates the request.
Does this capture requests from every browser tab?
The listener is attached to one Puppeteer Page. Add an appropriate listener to each page whose traffic you need to observe.
Can I use this to inspect a website screenshot request?
This method observes requests made by the page controlled by Puppeteer. For screenshot capture from a URL, ScreenshotNeo offers an API and MCP server; it is not a POST-body inspection tool.


