Follow a page request through the server
In this guide you send a few requests to your own server and read what comes back. Each request shows one stage a page goes through, so at the end you can tell from a response which part of the server answered it and why.
Every request goes through the same stages, in this order:
- Security headers, the request log and a request id are added.
- The server picks a handler from a route table that is fixed at start-up.
- For a public page, the page cache is asked first.
- Redirect rules are checked before any content.
- The address is matched to a published page, post or listing.
- The theme's templates build the HTML, which is sent and, when it is safe, cached.
Before you start
- A running Hatamoto site you can sign in to as an owner. To set one up, follow the install guide.
- A shell on the server, with
curl, and the service's settings loaded soDATABASE_URLis set. See how Hatamoto is put together shows how, in step 1. - Access to the server log:
journalctl -u hatamoto-apiwith systemd,docker compose logs hatamotowith Docker.
On Docker, put docker compose exec hatamoto in front of each hatamoto command; curl runs on the host, where the compose file publishes port 8090.
The examples call the server directly on http://127.0.0.1:8090, so your reverse proxy (the web server in front of Hatamoto that handles HTTPS) does not add or change headers.
Steps
- Request the home page and look at the headers only:
curl -s -o /dev/null -D - http://127.0.0.1:8090/The output, shortened:
HTTP/1.1 200 OK Cache-Control: public, max-age=60, s-maxage=300 Content-Security-Policy-Report-Only: default-src 'self'; ... Content-Type: text/html; charset=utf-8 X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-Request-Id: bd8bc1433d109735deeea10c6db995aeNo header says what built the page: the theme engine renders it and the response names no software. The security headers come from stage 1. The policy is report-only until you set
CSP_ENFORCE=true, andStrict-Transport-Securityis added only whenENVIRONMENT=production. - Send your own request id. The server keeps an
X-Request-IDyou send and makes one up when you do not:curl -s -o /dev/null -D - -H 'X-Request-ID: test-123' http://127.0.0.1:8090/ | grep -i request-idX-Request-Id: test-123A reverse proxy can set this header, so one id follows a request through both servers.
- Find the request in the log. Each request writes one line:
journalctl -u hatamoto-api --no-pager | grep 'http request' | tail -1With
ENVIRONMENT=production, as in Deploy with systemd, the log is JSON, one object per line:{"time":"2026-10-20T09:15:02.418Z","level":"INFO","msg":"http request","method":"GET","path":"/","remote_addr":"127.0.0.1:44012","status":200,"size":8231,"duration":2100000,"user_agent":"curl/8.22.0"}In any other environment the same line is plain text, such as
level=INFO msg="http request" method=GET path=/ status=200 size=8231 duration=2.1ms. The line has the method, path, status, size and time taken (in JSON,durationis in nanoseconds). It does not include the request id. - Ask for an address that does not exist:
curl -s -o /dev/null -D - http://127.0.0.1:8090/no-such-page | grep -E '^HTTP|Cache-Control'HTTP/1.1 404 Not Found Cache-Control: private, no-cache, no-storeThe body is your theme's 404 page. The miss is written to the 404 log, which you can see in the admin at Settings > Redirects,
/admin/settings/redirects/404. - Ask for a missing address under
/api:curl -s http://127.0.0.1:8090/api/nope{"error":"not found"}Addresses under
/apiand/mediaare never pages, so the server answers with JSON instead of a themed page. - Add a redirect and follow it. Redirect rules are checked before content, so a rule wins over a page at the same address:
hatamoto redirect create --from /old-page --to / curl -s -o /dev/null -D - http://127.0.0.1:8090/old-page | grep -E '^HTTP|Location'HTTP/1.1 301 Moved Permanently Location: /Remove the test rule. Use the id that
hatamoto redirect listprints in the first column:hatamoto redirect list hatamoto redirect delete <id> - Ask for the admin without signing in:
curl -s -o /dev/null -D - -H 'Sec-Fetch-Mode: navigate' -H 'Sec-Fetch-Dest: document' \ http://127.0.0.1:8090/admin/ | grep -E '^HTTP|Location|Cache-Control'HTTP/1.1 303 See Other Cache-Control: no-store Location: /admin/login?next=%2Fadmin%2FThe admin skips the page stages. It has its own stricter security policy, never lets a browser cache its pages, and sends you to sign in when there is no session. The two
Sec-Fetchheaders say "a top-level page load", which is what a browser sends from the address bar; without them the admin answers 404, so a script on a public page can never read an admin page.
Check it worked
You saw a page built by the theme (step 1), a 404 from the theme (step 4), a JSON 404 (step 5), a redirect (step 6) and the admin's sign-in redirect (step 7). Each response told you which stage answered it.
If step 1 returns 502 or connection refused, the server is not listening on that port. Check API_PORT in the settings file and systemctl status hatamoto-api. If step 6 prints DATABASE_URL is required, load the service's settings in your shell first.
The page cache in stage 3 is used only when the cache is on, the site's public address is set, the address has no query string (except ?page=2 and higher) and the page is not a preview. Those rules, and every other detail of each stage, are on Request flow.