How I check whether a "no upload" PDF tool really keeps my file local
Lots of PDF and image sites now say "your file never leaves your device". Some of them mean it, and some run the work on a server anyway. You don't have to take the label on trust: the browser can show you every request
Lots of PDF and image sites now say "your file never leaves your device". Some of them mean it, and some run the work on a server anyway. You don't have to take the label on trust: the browser can show you every request a page makes, and you can cut the network and see whether the tool still works. Below are the exact steps in Chrome or Edge (Firefox where it differs), what each check can and can't prove, and one trap with the obvious "look for a big request" approach. I checked the DevTools labels in Chrome 156 and against the Chrome and Firefox DevTools docs, and I ran the checks on a small test page of my own that does upload a file, to see what a real upload looks like.
Check 1: watch the Network panel while the tool runs
- Open the tool's page, but don't pick a file yet.
- Open DevTools (F12, or Ctrl+Shift+I; Cmd+Option+I on a Mac) and click the Network tab. DevTools only records requests while it's open, so open it before you pick the file.
- Tick Keep log. Older Chrome versions call it Preserve log. Without it the list is wiped if the page navigates, which is exactly when a "processing..." page might send your file.
- Click Clear (the circle with a line through it) so the list only holds what happens next.
- Now pick your file, run the tool, and wait until the download has finished. Then wait a bit longer before you read the list.
What to look for is any request that sends data out: method POST or PUT. Type method:POST in the Filter box to show only those (I checked that this filter works), or right-click a column header and add the Method column. Fetch/XHR in the type buttons is another useful view, since uploads from scripts usually show up there.
Click a suspicious request and look at:
- Headers: Request URL tells you where it went, and Request Method confirms it's a POST.
-
Payload: a normal form upload shows up here. On my test page, a file sent as form data appeared under "Form data" as
file (binary). - Initiator: which script started it.
The trap: the Size column is about the download
The obvious advice is "look for a request the size of your file". The Size column won't show it. Chrome's docs define Size as "the combined size of the response headers plus the response body, as delivered by the server", which is the download, not what was sent. On my test page, a 509,728-byte PDF uploaded with fetch showed up as 0.1 kB in the Size column, because the server only answered "ok". The status bar said "96 B transferred".
The size filter has the same problem. larger-than:100k hid that half-megabyte upload completely. So don't filter by size to find uploads; filter by method, then open each POST and see what's in it. In my test, the request that sent the file as a raw blob had no Payload tab at all. The Headers tab only showed "Provisional headers are shown" with Content-Type: application/pdf, which was enough to give it away.
The gotcha: workers and delays
An upload doesn't have to happen when you click the button, and it doesn't have to come from the page's main script. My test page hands the file to a Web Worker, which waits 3 seconds and then POSTs it. Chrome still listed it in the same Network panel, with a small gear icon in front of the name and worker.js:1 as the Initiator. But it only appeared 3 seconds after I picked the file. If I'd closed DevTools when the download finished, or cleared the list, I'd have missed it. Keep the panel open until well after the result is saved, and look again before you close the tab.
Firefox
The Network tab works the same way, with different names. The setting that keeps the log is Persist Logs, in the toolbar's menu of other actions (the docs also call it "Enable persistent logs" in Settings). The type buttons have XHR instead of Fetch/XHR. method:post works in the filter box. When you click a request, what was sent is in the Request tab rather than "Payload". The docs describe the Transferred and Size columns in terms of the resource being loaded, so I'd expect the same trap there. I didn't run the upload test in Firefox.
Check 2: cut the network after the page has loaded
A tool that really runs locally doesn't need the network once its code has loaded.
- Load the page and wait until it's fully ready.
- Go offline. In Chrome DevTools, open the throttling drop-down next to "Disable cache" (it says No throttling) and choose Offline. In Firefox it's the Throttling drop-down, which also has Offline. Or just turn off Wi-Fi or unplug the cable, which takes the whole machine offline rather than one tab.
- Pick the file and run the tool.
If it finishes and you get a working file, the work happened on your machine. If it hangs or shows a network error, it needs a server.
What these checks can't prove
Both checks show what happened while you were watching, on that one visit. Neither one can show:
- Later sending. A page can keep the file in the browser's storage and send it when you're back online or on your next visit. The offline test only proves the tool doesn't need the network to work.
- Small pieces. A file can be split into many small requests, or the page can send something derived from it (extracted text, a thumbnail), which would be much smaller than the file. That's why you open each POST instead of looking for one big one.
- Encoding. Base64 makes data about a third bigger and it can be wrapped in JSON, so the body won't look like a PDF.
- WebSockets. A WebSocket connection shows up as one entry; what's sent over it is under that entry's Messages tab (Chrome lists up to the last 100 messages, with a Length column). Click the Socket button (older versions say WS) to find these.
-
Anything outside the tab. DevTools shows that tab's traffic. A browser extension, another tab, or a desktop app isn't in that list. Chrome's
chrome://net-exportpage records the whole browser's network activity to a file while that page stays open; reading the file needs Chromium's netlog viewer, so it's more work, but it doesn't depend on which tab's DevTools you had open.
And they can't tell you what the site's code will do next week. A tool that's local today can change. If a file is sensitive enough that this matters, an offline command-line tool avoids the question. For merging, qpdf does it in one line (I used qpdf 12.4.2):
qpdf --empty --pages first.pdf second.pdf -- merged.pdf
What I saw on my own merge tool
I built ToolOcean, so I ran both checks on its merge page as well, with two dummy PDFs I generated (509,728 and 151,787 bytes; 2 pages and 1 page). I used headless Chrome driven by Playwright, logging every request with its method, host and request body size, and searching each body for chunks of either file, raw and base64 encoded.
- Loading the page made about 110 requests, almost all GETs to the site itself for its own code and fonts, plus a Google Tag Manager script and three small analytics POSTs (272 to 289 bytes) to gateway.umami.is.
- From picking the files through to the download, and for 20 seconds after it, there was one request: a 357-byte POST to gateway.umami.is. Its body is JSON recording a "tool-run" event for "pdf-merge", with the page URL, screen size and language. No file names and no file content.
- No request body contained any part of either PDF. The merged file was 3 pages, 659,579 bytes, with both files' text in order.
- With the browser set offline after the page had loaded, the merge still finished and saved a 3-page file of the same size. The only failure was that analytics ping, which errored with
ERR_INTERNET_DISCONNECTED.
The tool itself is at toolocean.co/merge-pdf-without-uploading. The checks above are the same ones I'd run on anyone else's.
Drafted with AI help, tested and checked by me.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.