HTTP na wetin? Guide for server admins
Understand HTTP from server side: methods, status codes, headers, nginx logs, caching, TLS, and how HTTP/3 changes the way requests reach your backend.
Wetin be HTTP?
HTTP (hypertext transfer protocol) na set of rules wey client and web server dey use to request something and send am back. Client dey send request: method like GET, path like /pricing, protocol version, list of headers, and sometimes body. Server dey answer with status code like 200, followed by e own headers and usually body. Every page view and every API (application programming interface) call for your server na this same exchange wey dey repeat.
HTTP no keep state by itself. Server no remember wetin you request one second ago, so anything wey behave like memory, like login session, dey carried inside header for every single request. This property explain plenty of wetin follow: caching depend completely on headers, and load balancer fit send your next request go different backend without causing problem.
Everything wey dey below show how this model look from server side, for your access log and inside your nginx config.
A raw request and response, annotated
Here na complete HTTP/1.1 request. Blank line dey end the headers, and anything after that line na the body. A GET normally no get body.
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGETna the method. E dey tell wetin you want make happen.GETdey read data,POSTdey send data,PUTdey replace data,DELETEdey remove data, whileHEADdey ask for the headers of aGETwithout the body./pricingna the path. Hostname no dey part of the request line, na why the next header dey exist.HTTP/1.1na the protocol version wey the client dey use.Host: example.comdey name the site wey the client want. HTTP/1.1 require am, so nginx go answer request wey no get am with400 Bad Request.- The remaining ones na preferences.
Accept-Encoding: gzipdey say the client fit decompress data, so server fit compress the body.
The response get the same shape, but status line dey on top.
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OKna the status code with the reason phrase. Na the code matter. The phrase na decoration, and clients dey ignore am.Content-Typedey tell the client how e suppose handle the bytes wey follow.Content-Lengthna the body size for bytes, so the client know where the body end. When server no know the size beforehand, e go sendTransfer-Encoding: chunkedinstead and mark the end with chunk wey get zero length.Cache-Controldey tell the browser and any cache wey dey between dem how long dem fit keep this response.- The blank line after the headers dey separate the headers from the body for both directions.
Header names no dey case-sensitive, and every line dey end with carriage return followed by line feed, instead of bare newline. You no go type these by hand, but you go see dem for packet capture.
To monitor real request and response pair, run this against site wey you own:
curl -sS -o /dev/null -D - https://example.com/-D - dey write the response headers for your terminal, while -o /dev/null dey throw the body away. Prefer this one over curl -I, because -I dey send a HEAD request. Application server wey handle HEAD differently from GET, and plenty of dem dey do so, go show you headers wey browser no ever receive. curl -v dey print both sides, with request lines marked > and response lines marked <.
How request line dey look for your nginx access log
nginx dey ship with a combined log format, and na so dem define am:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';One line wey e produce:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45na$remote_addr, the address wey open the TCP (transmission control protocol) connection. If proxy dey in front, na proxy address go show, no be visitor own.- The first
-na fixed placeholder. The second one na$remote_user, and e only get value when HTTP basic authentication dey use. "GET /pricing HTTP/1.1"na$request, the request line exactly as e arrive.200na the status wey your server return, no be the status wey visitor see.5310na$body_bytes_sent, the body only. Response headers no dey inside, so this number always small pass the bytes wey server actually send.- The last two quoted fields na
RefererandUser-Agent. Client dey provide both, so dem fit contain anything.
Because $request dey copy the request exactly, junk go show exactly as e come. Client wey dey speak TLS (transport layer security) to your plaintext port 80 go leave a 400 line wey request field start with escaped bytes like "\x16\x03\x01\x02\x00\x01". \x16 na the TLS handshake record type, so those bytes na the beginning of a ClientHello, no be request line at all. Your server dey behave correctly. Something dey point HTTPS to an HTTP port.
Add $server_protocol to your log format too. E dey print HTTP/1.1, HTTP/2.0 or HTTP/3.0, and na the fastest way to prove say protocol change don actually take effect.
Meaning of common status codes when your own site dey return dem
The first digit na the class, and na the class you suppose read first.
2xx mean say e work. 200 OK na for normal read. 201 Created after POST wey create something. 204 No Content na success wey no get anything to send back, and na the usual answer to DELETE.
3xx mean say make you look another place. 301 permanent, and browsers dey cache am well-well, sometimes until user clear im profile. So, 301 wey point to wrong hostname fit hard to undo. Use 302 while you still dey test redirect. 304 Not Modified na success, no be error: client send If-None-Match wey carry ETag (entity tag) wey you still recognise, so you reply with headers and no body. If log full of 304s, e mean say caching dey work.
4xx mean say request get problem. 400 Bad Request na malformed input. 401 Unauthorized really mean say client no authenticate, and e must carry WWW-Authenticate header wey name the scheme. 403 Forbidden mean say server understand the request but refuse am anyway. 404 Not Found na path wey no exist. 405 Method Not Allowed na correct path with wrong method, na wetin POST to static file location dey return. 413 na body wey pass nginx's client_max_body_size, wey default na 1 megabyte, and error log confirm am with client intended to send too large body.
A 403 for static file almost always mean filesystem problem, no be HTTP rule. Read /var/log/nginx/error.log before you change any config. open() "/srv/site/index.html" failed (13: Permission denied) mean say nginx worker user no fit read the file, most times because parent directory no get execute permission for others. directory index of "/srv/site/" is forbidden mean say path resolve to directory wey no get index file while autoindex dey off.
5xx mean say problem dey your side. 500 na error wey your application no handle. 502 Bad Gateway mean say nginx no fit get usable response from upstream, and error log go name the cause: connect() failed (111: Connection refused) while connecting to upstream mean say nothing dey listen for address inside proxy_pass. 504 Gateway Timeout mean say upstream accept the connection, then no talk anything within proxy_read_timeout, wey default na 60 seconds, and log record am as upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable na deliberate refusal. Note say nginx own rate limiter dey return 503, because limit_req_status default na 503. If you dey search for 429 Too Many Requests for your log but you dey see 503 instead, na why. Set limit_req_status 429; to get the correct code.
Headers wey matter when you dey run the server
Host dey pick the site. One IP address fit serve hundreds of hostnames, and nginx dey match Host against server_name to decide which server block go answer. If nothing match, nginx dey use the default server, wey be the first block wey dey listen for that address and port unless dem mark another one as default_server. If new virtual host dey return wrong site, na almost always because the name no match, so the request fall through to the default. Test am without touching DNS:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent na self-description wey client write, and e be free text. Use am as hint when you dey read logs. Never use am as control, because client wey wan lie about am go simply do so. So blocking scraper by User-Agent go filter only the polite ones.
Content-Type dey decide how to interpret the bytes: application/json for API request, text/html; charset=utf-8 for page. nginx dey map file extensions to types with /etc/nginx/mime.types, and the packaged nginx.conf dey set default_type application/octet-stream;. So if file get extension wey nginx no know, e go offer am as download instead of render am. The visible sign na page wey load without styling while browser console dey print Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME here mean multipurpose internet mail extensions, the naming scheme wey those type strings come from.
Cache-Control na how you control every cache between your server and the reader. public, max-age=31536000, immutable good for assets wey filename contain content hash, because the name dey change when the content change. no-store belong on anything wey specific to user, because shared cache wey keep logged-in page fit give am to the next person wey ask for the same URL. private na the middle setting: browser fit keep am, but shared cache no fit.
X-Forwarded-For dey exist because proxy dey hide visitor address. Once request pass through reverse proxy, $remote_addr na the proxy address, so your logs, geolocation, and rate limiting all go see one client. The proxy must pass the original address along:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;The receiving server must then get instruction to trust am, and exactly who e suppose trust:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;List only ranges wey you control. X-Forwarded-For na plain text wey any client fit send, so set_real_ip_from 0.0.0.0/0; lets visitor choose the address wey you log and the address wey your rate limiter count.
X-Forwarded-Proto dey prevent one specific and common failure. Your proxy terminate TLS and forward request to the application over plain HTTP. Application see plain request, decide say visitor suppose dey on HTTPS, and answer 301 https://example.com/. Browser follow am, proxy terminate TLS again and forward plain HTTP again, and the loop repeat until browser give up with ERR_TOO_MANY_REDIRECTS. Sending X-Forwarded-Proto: https tells application say visitor don already dey on HTTPS, so e stop redirecting.
HTTP/1.1 vs HTTP/2 vs HTTP/3: wetin change for you
HTTP/1.1 na text, and e dey handle one request at a time for each connection. Connection: keep-alive allow the next request reuse the same TCP connection, wey save the setup cost, but responses still dey return in the order wey dem request am. One slow response go block everything wey dey queue behind am. Na head-of-line blocking be this, and browsers dey work around am by opening several connections to the same hostname at once.
HTTP/2 keep the same methods and the same status codes, but e change the framing to binary. Many requests dey share one connection as independent streams, and repeated header text dey compressed. This matter because modern request dey carry plenty of header text. The connection still na TCP, so one lost packet go stall every stream for that connection until retransmission arrive. The head-of-line blocking no disappear. E move down from HTTP go transport layer. Server push dey part of HTTP/2 before, but for practical use e don go because Chrome remove support for am in 2022.
HTTP/3 keep the same semantics again and replace TCP with QUIC, wey be transport built on UDP (user datagram protocol). QUIC streams dey independent reach down, so lost packet go stall only the stream wey e belong to. TLS 1.3 dey built inside the QUIC handshake instead of layering am on top, so new connection need fewer round trips. Two practical results follow: UDP port 443 must open for every firewall wey traffic pass through, and any network wey dey throttle or block UDP go make clients fall back to HTTP/2.
Wetin change for you, for clear terms. Browsers no dey start with HTTP/3. Dem connect over HTTP/2 or HTTP/1.1, see an Alt-Svc: h3=":443"; ma=86400 header for the response, then use HTTP/3 for later connections to that host. So the header no be optional decoration. Na the discovery mechanism be that. For nginx, HTTP/2 become its own directive for version 1.25.1 (http2 on; inside the server block, replacing the old listen ... http2 parameter), and QUIC arrive for mainline 1.25.0, where HTTP/3 site need listen 443 quic reuseport; together with the ordinary listen 443 ssl;.
Proxies no get the same level of maturity for this area, and you suppose check am against the version wey you dey run. As of August 2026, Caddy dey serve HTTP/3 by default without configuration. nginx need the explicit quic listener plus the Alt-Svc header wey we describe above. Traefik enable am per entry point through explicit http3 option. If you terminate TLS at Traefik wey dey front several Docker apps, the protocol version wey your visitors get na there dem decide am, and the hop from the proxy go your container usually na plain HTTP/1.1, no matter wetin browser negotiate.
Verify instead of assuming. curl --http3 -sS -o /dev/null -D - https://example.com/ go work only if curl -V list HTTP3 among its features, and most distribution builds no include am. The dependable check na your own log: add $server_protocol to the format and read wetin real browsers negotiate. Before you do any of that, confirm say UDP 443 really dey open, because firewall wey allow only TCP 443 go make HTTP/3 fail quietly while the site continue to work over HTTP/2. Knowing which ports dey open and listening for your Linux server na the first thing to check.
HTTPS: HTTP na protocol, TLS na wrapper
HTTPS no be separate protocol. Na the same requests and status codes wey dey pass inside TLS session. Port 80 dey carry dem without encryption, while port 443 dey carry dem encrypted. TLS handshake go complete first, then HTTP request go travel inside the encrypted channel. Na why certificate problem no dey get status code: the failure happen before any HTTP byte send, so no response dey to number.
One ordering detail important for server wey dey host many sites. Server dey choose certificate with SNI (server name indication), wey be field for TLS handshake wey carry hostname in clear before any HTTP header exist. So server first choose certificate from SNI, then choose virtual host from the Host header. Na two separate lookups wey normally dey agree. When dem no agree, browser go show name mismatch like NET::ERR_CERT_COMMON_NAME_INVALID and e no go send any request, because default server certificate offer for name wey e no cover.
For public site, get real certificate and make e renew by itself. Certbot with Let's Encrypt for nginx dey write certificate paths inside your server block and install renewal timer for you. For hostname wey no public authority fit validate, like internal name or bare IP address for your own network, self-signed certificate for Ubuntu na the honest option, as long as you accept say every client must be told to trust am.
Once TLS dey work, send everything for port 80 go port 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Add Strict-Transport-Security only when you sure. The add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; header tells browsers make dem refuse plain HTTP for that hostname for two years, and dem dey obey am from their own cache. This means say removing the header later no go undo am. Start with max-age of few hours, confirm say every subdomain really dey on HTTPS, then increase am.
FAQ
Wetin be the difference between HTTP and HTTPS?
HTTPS na HTTP wey dey inside TLS (transport layer security) session. The methods and status codes dey identical. The change be say bytes dey encrypted between the client and anything wey terminate TLS, and the default port move from 80 to 443. Because TLS handshake finish before the first HTTP byte send, certificate failure no dey produce status code. Na why browser certificate warning dey show error name like NET::ERR_CERT_COMMON_NAME_INVALID instead of number like 403.
Why my site dey return 502 Bad Gateway?
A 502 from nginx mean say nginx no fit get usable response from the upstream wey e proxy to. So visitor request dey fine, but something behind nginx no dey fine. Read /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream mean say nothing dey listen on the address and port for proxy_pass. Check say the application dey run and e bind for the place wey you expect. no live upstreams while connecting to upstream mean say every server for the upstream block don mark as down after repeated failures. Compare am with 504 Gateway Timeout. This one mean say upstream accept the connection, then fail to answer within proxy_read_timeout.
Why my access log dey show the same IP address for every visitor?
Because $remote_addr dey record the address wey open the TCP connection. When reverse proxy or content delivery network dey involved, na the proxy address e go record. Visitor address dey arrive inside X-Forwarded-For header instead. Set proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; for the proxy. Then for the receiving nginx, set set_real_ip_from to the proxy address range and real_ip_header X-Forwarded-For;. List only ranges wey you control, because any client fit send that header as text. If you trust am from the whole internet, visitor fit choose the address wey you log and the address wey you rate limit.
I need turn on HTTP/2 or HTTP/3?
HTTP/2 worth turning on, because na one directive for site wey already get TLS, and e remove the per-connection request limit wey dey make page with plenty small files slow. HTTP/3 get smaller and less certain benefit, and e need open UDP port 443 plus proxy build wey get QUIC support. Remember say browsers go switch to HTTP/3 only after dem see an Alt-Svc header for earlier response. Without that header, nothing go change no matter wetin your listen line talk. Add $server_protocol to your log format and measure wetin visitors actually negotiate before you spend time on am.
Wetin 403 Forbidden mean when the file dey exist?
For static site, a 403 usually na filesystem permission problem, no be HTTP rule. open() ... failed (13: Permission denied) for /var/log/nginx/error.log mean say nginx worker user no fit read the file. Most times, na because parent directory no get execute permission for others, no be because the file mode wrong. directory index of ... is forbidden mean say request resolve to directory wey no get index file, while autoindex dey off. Explicit deny rule for the matching location block also dey return 403. So read that block when error log no talk anything.