Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

The HEAD Layer of app.onepassapps.com: What a Request With No Body Asks For

I work on OnePassApps - this is our own site, and every number below was measured on it today, 10 November 2026, over plain HTTPS from outside the browser: signed out, no cookies, no JavaScript, one build, one edge locat

I work on OnePassApps - this is our own site, and every number below was measured on it today, 10 November 2026, over plain HTTPS from outside the browser: signed out, no cookies, no JavaScript, one build, one edge location.

A GET asks for a page and gets the page back. A HEAD asks the same question and gets the description only - the status, the headers, no body. It is the cheapest way to ask a host what it thinks it has, and link checkers, crawlers and uptime monitors ask it constantly.

So I sent both to every URL in our sitemap, to every static file our pages point at, and to the API host behind the apps, and compared the two answers field by field. The result is sharper than I expected: on our pages the two methods agree on everything except one header, and on one file they disagree about the size of the thing.

The two methods, side by side

All 56 URLs in our sitemap answered the HEAD request with HTTP 200, the same status the GET returned on every one of them, and every HEAD answer came back with an empty body.

What the answer carried HEAD GET
HTTP 200 56 of 56 56 of 56
Content-Length 0 of 56 0 of 56
Transfer-Encoding 0 of 56 56 of 56
Content-Type 56 of 56 56 of 56
Cache-Control 56 of 56 56 of 56
CF-Cache-Status 56 of 56 56 of 56
Age 0 of 56 0 of 56
ETag 0 of 56 0 of 56
Last-Modified 0 of 56 0 of 56
Set-Cookie 0 of 56 0 of 56

Two rows carry the whole story. Not one of the 56 HEAD answers and not one of the 56 GET answers carried a Content-Length header - the server never tells you how big a page is. The GET says how the body is framed instead: Transfer-Encoding: chunked on all 56 of them. A HEAD says nothing about framing at all.

The header names themselves tell the same story. A GET on our pages carries 22 distinct header names and a HEAD carries 21. The only name that changes is transfer-encoding: it comes back on the GET and not on the HEAD.

Three real answers

app.onepassapps.com/

  • HEAD -> HTTP 200, Content-Length: absent, Transfer-Encoding: absent, Cache-Control: public, max-age=0, must-revalidate, Age: absent
  • GET -> HTTP 200, Content-Length: absent, Transfer-Encoding: chunked, Cache-Control: public, max-age=0, must-revalidate, Age: absent

app.onepassapps.com/terms

  • HEAD -> HTTP 200, Content-Length: absent, Transfer-Encoding: absent, Cache-Control: public, max-age=0, must-revalidate, Age: absent
  • GET -> HTTP 200, Content-Length: absent, Transfer-Encoding: chunked, Cache-Control: public, max-age=0, must-revalidate, Age: absent

app.onepassapps.com/apps/paperkit

  • HEAD -> HTTP 200, Content-Length: absent, Transfer-Encoding: absent, Cache-Control: public, max-age=0, must-revalidate, Age: absent
  • GET -> HTTP 200, Content-Length: absent, Transfer-Encoding: chunked, Cache-Control: public, max-age=0, must-revalidate, Age: absent

app.onepassapps.com/assets/app-shell-shared-Dsj7SbLS.js

  • HEAD -> HTTP 200, Content-Length: 369669, Transfer-Encoding: absent, Cache-Control: public, max-age=31536000, immutable, Age: 76491
  • GET -> HTTP 200, Content-Length: 369669, Transfer-Encoding: absent, Cache-Control: public, max-age=31536000, immutable, Age: 76491

The hashed assets are the opposite

The 5 files under /assets/ are the opposite of a page. Every one of them answers both methods with a Content-Length header, and the two answers agree on the number to the byte - 369669 bytes on the app shell script, 255426 on the CSS, 178924 on the vendor bundle, 325705 on the entry script and 694 on the runtime.

They also carry an Age header on both methods, and the Age counter moves between the two calls: on the entry script it read 76490 on the HEAD and 76491 on the GET a moment later, and on the stylesheet 133639 and 133640. That is a live counter on the copy the edge is serving, not a build timestamp, and a HEAD request does not freeze it.

All 5 are served with Cache-Control: public, max-age=31536000, immutable and answer CF-Cache-Status: HIT on both methods.

The root static files split three ways

The 6 files at the root of the site are not pages and not hashed assets, and they answer in three different shapes:

  • app.onepassapps.com/apple-touch-icon.png - Content-Length on HEAD: 3492, on GET: 3492; CF-Cache-Status: REVALIDATED
  • app.onepassapps.com/favicon.ico - Content-Length on HEAD: 3556, on GET: 3556; CF-Cache-Status: REVALIDATED
  • app.onepassapps.com/favicon.svg - Content-Length on HEAD: absent, on GET: absent; CF-Cache-Status: REVALIDATED
  • app.onepassapps.com/shell-fallback.css - Content-Length on HEAD: absent, on GET: absent; CF-Cache-Status: REVALIDATED
  • app.onepassapps.com/shell-fallback.js - Content-Length on HEAD: absent, on GET: absent; CF-Cache-Status: REVALIDATED
  • app.onepassapps.com/site.webmanifest - Content-Length on HEAD: absent, on GET: 876; CF-Cache-Status: DYNAMIC

Two of them carry a length on both methods. Three carry none on either - their GET is chunked, their HEAD is silent about framing, and their edge status is REVALIDATED, so the edge has a copy it re-checks rather than one it hands straight out of storage.

The sixth one is the only file in the whole set where the two methods disagree about the size: app.onepassapps.com/site.webmanifest answers the GET with a Content-Length of 876 bytes and the HEAD with no length at all. It is also the only root static file marked DYNAMIC rather than REVALIDATED.

The API host answers HEAD too

The API host behind the apps is a different service on the same edge, and it answers the same question. HEAD on the product catalogue answers HTTP 200 with Content-Type: application/json and Cache-Control: private, no-store, max-age=0, while the GET on the same route answers HTTP 200 and 15650 bytes of JSON with the same two headers.

A HEAD on a route that does not exist answers HTTP 404, so a HEAD is enough to find out whether a record is there without pulling a byte of the catalogue. Both methods on the API host are marked CF-Cache-Status: DYNAMIC and carry Cache-Control: private, no-store, max-age=0 - it is the one host of ours that asks the browser not to keep anything.

What HEAD does not tell you

Everything above is about the shell. A HEAD does not run the page, so nothing in this post says what any of our apps do - it says what the server is willing to promise about them before a byte of the document is sent. Our pages are client-rendered, so the honest reading of a HEAD on our site is: correct status, correct type, correct cache policy, and no idea how big it is.

The limits of this measurement

  • One build, one day, one region: every cf-ray value on these pages ended in EWR, so all 56 page answers came from the same edge location.
  • No cookies, no session, no JavaScript, and Accept-Encoding: identity on every request, so the two methods were compared on the same bytes.
  • 67 targets - 56 pages, 5 hashed assets and 6 root static files - each fetched with both methods, plus a second header-name sweep over the 56 pages and four requests to the API host.
  • A HEAD on a page does not execute it, so this is not a measurement of what the apps do. It is a measurement of what the edge says about them.

The apps themselves are listed at OnePassApps, and every tool has a page of its own at the app guides.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.