DEV Community

Edem DOGBE
Edem DOGBE

Posted on

Does That Online PDF Tool Upload Your File? A Network Test You Can Run Yourself

Most online PDF tools tell you the same thing: your files are deleted after an hour. That sentence answers a question nobody asked. A server can only delete a file it has received, so the real question is whether the file left your machine at all, and when.

You don't need to trust anyone's answer to that. The browser already records every request a page makes. With the developer tools open and a test file, you can see an upload happen, measure it, and find out which server received it. Here is the method we used for a network measurement of popular PDF tools, written so you can repeat it on any site.

Prepare a file you can afford to leak

Never test with a real document. Generate a PDF with obviously fake content ("TEST DOCUMENT", a made-up client, a made-up amount) and note its exact size in bytes. Ours was eight pages. The size matters more than the content: you'll recognise the upload because a request body roughly that large shows up in the log.

Set up the Network panel

In Chrome or Edge, open DevTools (F12) and go to the Network tab. Three settings change what you see:

  • Preserve log. Without it, the log is wiped when the page navigates, and some tools redirect to a result page right after upload.
  • Disable cache. Optional, but it makes the page download its code for real, so you can tell code loading apart from everything else.
  • Clear the log just before you drop the file. Everything that appears afterwards happened because of your file.

Firefox has the same thing: Network Monitor, with "Persist Logs" in the settings menu.

Now drop the test file into the tool and watch. Don't click anything else yet.

What an upload looks like

An upload is a request that carries a body. In practice that means a POST or PUT, usually multipart/form-data, and a request body about the size of your file.

Type method:POST in the filter box to hide everything else. For each remaining request, open it and check two places:

  • the Headers tab, under Request Headers: Content-Length and Content-Type;
  • the Payload tab, which shows the form fields being sent, usually including the file name.

Careful with the Size column in the main table. It shows what came back, not what went out. A 2 MB upload that gets a short JSON reply shows a few hundred bytes there. Request size is only visible inside the request.

Then look at the timing. On 29 August 2026 we ran this test on the compression tool of four popular services. Three of them (iLovePDF, PDF2Go and PDF24 Tools) sent the file as soon as it was dropped, before any "Compress" button was clicked. One of them also fired a request to Microsoft Clarity, a session-analytics service, during the operation. For the fourth, Smallpdf, our automated harness couldn't trigger the native file picker, so we recorded it as not measured instead of guessing. Infrastructure changes, so these results hold for that date only.

None of this is hidden or illegal. It is how a server-side tool works: the server is the engine, so the file has to travel. The test simply lets you see it instead of inferring it from a privacy page.

Find out where it went

The request URL gives you a hostname, such as api110.ilovepdf.com. Two commands turn that into a location:

dig +short api110.ilovepdf.com
whois 57.129.84.252 | grep -iE 'org-name|orgname|country|netname'
Enter fullscreen mode Exit fullscreen mode

The first resolves the host to an IP address, the second asks the regional registry who holds that address block. In our run the uploads ended up with OVH and Hetzner, both in Germany. Two caveats: a CDN or load balancer in front of the real server will show the CDN's address, and the IP you get can depend on where you resolve from. Write down the date and your location next to the result.

What is not an upload

A page that processes files locally still makes requests. Learning to read them is what keeps this test honest.

Code downloads. A browser-only tool has to fetch its engine before it can run. In VellumPDF, pdf-lib is loaded with a dynamic import() the first time it's needed rather than on page load, and heavier engines, such as qpdf compiled to WebAssembly, arrive the same way when a tool calls for them. With Disable cache on, you'll see .js and .wasm files arrive after the drop. They are GET requests with no request body. Data comes in; nothing about your file goes out.

Analytics. Most sites send page views somewhere. Check what the body contains. Our own site, once a tool has been used, sends one usage event carrying the tool's identifier and nothing else: no file name, size or content. It deliberately sends it when you leave the page, not during processing. A strict test should still show it to you: navigate away with Preserve log on and you'll see it.

Model downloads. Some local features need large data files, such as OCR language models. Those are downloads too, and a well-behaved page tells you before it fetches them.

The rule: anything with a request body during processing deserves a look. Anything without one is the page receiving, not sending.

Read the page's own rules

One response header tells you which servers a page is allowed to contact at all. Open the main document request and find Content-Security-Policy, then the connect-src directive. Ours reads:

connect-src 'self' https://huggingface.co https://*.huggingface.co https://*.hf.co https://raw.githubusercontent.com
Enter fullscreen mode Exit fullscreen mode

That means scripts on the page can only fetch its own origin and the hosts that serve models. It's a strong hint, not proof: a CSP doesn't stop a page from uploading to its own origin, and a site can change its header tomorrow. Combine it with the network log.

Blind spots of the method

The Network panel is good, not perfect. Know where it can miss things:

  • Chunked uploads. A large file may be split into many requests. Add up the request bodies instead of looking for one large one.
  • Deferred sends. A page can queue data and send it with navigator.sendBeacon or fetch(..., { keepalive: true }) when you close the tab. Keep Preserve log on and navigate away before you conclude anything.
  • WebSockets. Frames are under the request's Messages tab, not in the request list.
  • WebRTC. Data channels don't appear in the Network panel at all. Chrome shows them in chrome://webrtc-internals.
  • Native file pickers and extensions. Automated harnesses struggle with some upload widgets, which is why one service in our table is marked not measured. A manual run with DevTools open avoids that.

The offline check, and its limit

A quicker test: load the tool, process a file once, then turn off your network and process another. If it works offline, that operation doesn't need a server. That's a clean positive result.

A failure proves less. The tool might simply not have downloaded its engine yet, because lazy-loaded code can't arrive without a connection. Run the operation once online first so the code is loaded, then cut the connection.

Measuring from inside the page

We wanted users to get this information without opening DevTools, so the result panel of our compress tool, shared by the other tools, reports how many bytes the page sent since the file was dropped. The Performance API won't tell you that: PerformanceResourceTiming has transferSize and encodedBodySize, but both describe the response. Request bodies are invisible to it.

So the page wraps the sending functions before anything else runs and measures each body as it goes out:

const sent = []
const originalFetch = window.fetch.bind(window)

window.fetch = (input, init) => {
  const url = typeof input === 'string' ? input : input.url
  sent.push({ t: performance.now(), url, bytes: bodySize(init?.body) })
  return originalFetch(input, init)
}

function bodySize(body) {
  if (body == null) return 0
  if (typeof body === 'string') return new TextEncoder().encode(body).byteLength
  if (body instanceof Blob) return body.size
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return body.byteLength
  return 1 // a stream: size unknown, never report zero
}
Enter fullscreen mode Exit fullscreen mode

The real version also wraps XMLHttpRequest.prototype.send and navigator.sendBeacon, handles FormData and Request objects, and uses the resource timeline to count loads from other origins.

It has limits we state on the privacy page. Requests made inside a Web Worker go through the worker's own fetch, so the window's wrapper never sees them. In our case those are engine downloads from our own domain, which DevTools does show. The bigger limit is a matter of principle: a page reporting on itself is a self-report. Code that wanted to cheat could bypass its own wrapper. The counter is a convenience; the Network panel is the evidence. That's the reason to learn the two-minute test above rather than take anyone's badge, including ours, at face value.

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate •

The self-report caveat is important. I'd soften "anything without a body is receiving, not sending": a GET can still send file-derived data in the URL or headers, so a zero request-body count is narrower than "nothing about the file left the browser." A synthetic filename/marker check across URLs, headers, worker traffic and deferred sends would complement the byte counter without using a real document. The separate offline test is a useful check of whether the operation needs a server, rather than proof that it never sends anything.