SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

Wat is HTTP? Een gids voor serverbeheerders

Begrijp hoe HTTP werkt vanuit het perspectief van uw server. Leer alles over methoden, statuscodes, headers, nginx logs en de implementatie van HTTP/3 en TLS in uw infrastructuur.

Wat is HTTP?

HTTP (hypertext transfer protocol) is de set regels die een client en een webserver gebruiken om gegevens op te vragen en te versturen. De client verstuurt een verzoek: een methode zoals GET, een pad zoals /pricing, een protocolversie, een lijst met headers en soms een body. De server antwoordt met een statuscode zoals 200, gevolgd door eigen headers en meestal een body. Elke paginaweergave en elke API (application programming interface) aanroep op uw server is die ene uitwisseling, herhaald.

HTTP houdt zelf geen status bij. De server onthoudt niet wat u een seconde geleden heeft gevraagd, dus alles wat zich als geheugen gedraagt, zoals een inlogsessie, wordt bij elk verzoek in een header meegegeven. Die ene eigenschap verklaart veel van wat volgt: caching wordt volledig door headers aangestuurd en een load balancer kan uw volgende verzoek naar een andere backend sturen zonder dat er iets stukgaat.

Alles hieronder laat zien hoe dat model er vanaf de serverkant uitziet, in uw access log en in uw nginx configuratie.

Een onbewerkte request en response, voorzien van annotaties

Hieronder staat een volledige HTTP/1.1-request. Een lege regel markeert het einde van de headers; alles wat daarna komt is de body. Een GET heeft doorgaans geen body.

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET is de methode, die aangeeft wat er moet gebeuren. GET leest data, POST verstuurt data, PUT vervangt data, DELETE verwijdert data, HEAD vraagt om de headers van een GET zonder de body.
  • /pricing is het pad. De hostnaam maakt geen deel uit van de request-regel; daarom is de volgende header noodzakelijk.
  • HTTP/1.1 is de protocolversie die de client gebruikt.
  • Host: example.com specificeert de site die de client wil bereiken. HTTP/1.1 vereist dit; daarom antwoordt nginx op een request zonder hostnaam met 400 Bad Request.
  • De overige regels zijn voorkeuren. Accept-Encoding: gzip geeft aan dat de client decompressie ondersteunt, waardoor de server de body mag comprimeren.

De response heeft dezelfde structuur, met een statusregel bovenaan.

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 OK is de statuscode met de bijbehorende reden. De code is bepalend. De tekst is decoratief en wordt door clients genegeerd.
  • Content-Type vertelt de client hoe de volgende bytes moeten worden behandeld.
  • Content-Length is de grootte van de body in bytes, zodat de client weet waar de body eindigt. Wanneer de grootte vooraf niet bekend is, verstuurt de server Transfer-Encoding: chunked en markeert het einde met een chunk van lengte nul.
  • Cache-Control geeft aan de browser en eventuele tussenliggende caches door hoe lang zij deze response mogen bewaren.
  • De lege regel na de headers scheidt deze van de body, in beide richtingen.

Headernamen zijn hoofdletterongevoelig en elke regel eindigt met een carriage return gevolgd door een line feed, in plaats van een enkele newline. U zult deze niet handmatig typen, maar u zult ze tegenkomen in een packet capture.

Voer het volgende commando uit tegen een site die u beheert om een echt paar te bekijken:

curl -sS -o /dev/null -D - https://example.com/

-D - schrijft de response-headers naar uw terminal en -o /dev/null negeert de body. Gebruik dit liever dan curl -I, omdat -I een HEAD-request verstuurt. Een applicatieserver die HEAD anders afhandelt dan GET — wat vaak voorkomt — zal u dan headers tonen die geen enkele browser ooit ontvangt. curl -v toont beide kanten, waarbij request-regels gemarkeerd zijn met > en response-regels met <.

Hoe de request-regel eruitziet in uw nginx access log

nginx levert een combined logformaat, en dit is de definitie ervan:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Eén regel die hierdoor wordt gegenereerd:

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.45 is $remote_addr, het adres dat de TCP (transmission control protocol) verbinding opende. Achter een proxy is dit de proxy zelf, niet de bezoeker.
  • De eerste - is een vaste tijdelijke aanduiding. De tweede is $remote_user, die alleen wordt ingevuld wanneer HTTP basic authentication in gebruik is.
  • "GET /pricing HTTP/1.1" is $request, de request-regel exact gekopieerd zoals deze binnenkwam.
  • 200 is de status die uw server teruggaf, niet de status die de bezoeker waarnam.
  • 5310 is $body_bytes_sent, enkel de body. Response-headers worden niet meegeteld, dus dit getal is altijd kleiner dan het aantal daadwerkelijk verzonden bytes.
  • De laatste twee velden tussen aanhalingstekens zijn Referer en User-Agent. Beide zijn afkomstig van de client, dus beide kunnen willekeurige inhoud bevatten.

Omdat $request letterlijk wordt gekopieerd, verschijnt rommel ook letterlijk. Een client die TLS (transport layer security) spreekt naar uw plaintext poort 80, laat een 400 regel achter waarvan het request-veld begint met geëscapete bytes zoals "\x16\x03\x01\x02\x00\x01". \x16 is het type TLS handshake-record, dus die bytes vormen het begin van een ClientHello en zijn in feite geen request-regel. Uw server gedraagt zich correct. Iets stuurt HTTPS-verkeer naar een HTTP-poort.

Voeg ook $server_protocol toe aan uw logformaat. Dit print HTTP/1.1, HTTP/2.0 of HTTP/3.0, en het is de snelste manier om aan te tonen dat een protocolwijziging daadwerkelijk effect heeft gehad.

Wat de algemene statuscodes betekenen wanneer uw eigen site deze teruggeeft

Het eerste cijfer geeft de klasse aan; dit is wat u als eerste moet lezen.

2xx betekent dat het is gelukt. 200 OK voor een normale leesactie. 201 Created na een POST die iets heeft aangemaakt. 204 No Content voor een succesvolle actie waarbij niets hoeft te worden teruggestuurd, wat het gebruikelijke antwoord is op een DELETE.

3xx betekent dat u elders moet kijken. 301 is permanent en browsers cachen dit agressief, soms totdat de gebruiker het profiel wist. Een 301 die naar de verkeerde hostnaam wijst, is daarom lastig te herstellen. Gebruik 302 terwijl u nog aan het testen bent met een redirect. 304 Not Modified is een succes, geen fout: de client stuurde een If-None-Match met een ETag (entity tag) die u nog herkent, dus u antwoordde met headers maar zonder body. Een log vol met 304-codes betekent dat caching werkt.

4xx betekent dat het verzoek onjuist was. 400 Bad Request duidt op onjuist geformatteerde invoer. 401 Unauthorized betekent in feite niet-geauthenticeerd en moet een WWW-Authenticate-header bevatten die het schema benoemt. 403 Forbidden betekent dat het verzoek werd begrepen, maar desondanks werd geweigerd. 404 Not Found is een pad dat niet bestaat. 405 Method Not Allowed is het juiste pad met de verkeerde methode; dit is wat een POST naar een locatie met statische bestanden teruggeeft. 413 is een body die groter is dan de client_max_body_size van nginx, die standaard op 1 megabyte staat; het foutenlogboek bevestigt dit met client intended to send too large body.

Een 403 op een statisch bestand heeft bijna altijd te maken met het bestandssysteem in plaats van met een HTTP-regel. Lees /var/log/nginx/error.log voordat u de configuratie wijzigt. open() "/srv/site/index.html" failed (13: Permission denied) betekent dat de nginx-workergebruiker het bestand niet kan lezen, meestal omdat een bovenliggende map geen uitvoerrechten (execute permission) heeft voor anderen. directory index of "/srv/site/" is forbidden betekent dat het pad naar een map leidde die geen indexbestand bevat terwijl autoindex is uitgeschakeld.

5xx betekent dat er aan uw kant iets is misgegaan. 500 is een onafgehandelde fout in uw applicatie. 502 Bad Gateway betekent dat nginx geen bruikbaar antwoord kon krijgen van de upstream, en het foutenlogboek benoemt de oorzaak: connect() failed (111: Connection refused) while connecting to upstream betekent dat er niets luistert op het adres in proxy_pass. 504 Gateway Timeout betekent dat de upstream de verbinding accepteerde en vervolgens niets meer zei binnen proxy_read_timeout (standaard 60 seconden), wat het logboek registreert als upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable is een bewuste weigering. Merk op dat de eigen rate limiter van nginx 503 teruggeeft, omdat limit_req_status standaard op 503 staat. Als u in uw logboek zoekt naar 429 Too Many Requests en in plaats daarvan 503 vindt, dan is dat de reden. Stel limit_req_status 429; in om de nauwkeurige code te krijgen.

De headers die ertoe doen wanneer u de server draait

Host kiest de site. Eén IP-adres kan honderden hostnamen bedienen en nginx vergelijkt Host met server_name om te bepalen welk server-blok antwoordt. Als niets overeenkomt, gebruikt nginx de standaardserver; dit is het eerste blok dat op dat adres en die poort luistert, tenzij een ander blok is gemarkeerd als default_server. Het krijgen van de verkeerde site bij een nieuwe virtual host komt bijna altijd hierdoor: de naam kwam niet overeen, waardoor het verzoek bij de standaardserver terechtkwam. Test dit zonder DNS aan te passen:

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent is een zelfbeschrijving geschreven door de client en is vrije tekst. Gebruik dit als aanwijzing bij het lezen van logs. Gebruik het nooit als controlemechanisme, want een client die erover wil liegen doet dat simpelweg. Het blokkeren van een scraper via User-Agent filtert daarom alleen de beleefde scrapers eruit.

Content-Type bepaalt hoe de bytes worden geïnterpreteerd: application/json voor een API-verzoek, text/html; charset=utf-8 voor een pagina. nginx koppelt bestandsextensies aan types met /etc/nginx/mime.types en het meegeleverde nginx.conf stelt default_type application/octet-stream; in, waardoor een bestand met een extensie die nginx niet kent als download wordt aangeboden in plaats van gerenderd. Het zichtbare symptoom is een pagina die laadt zonder opmaak, terwijl de browserconsole Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type weergeeft. MIME staat voor multipurpose internet mail extensions, het naamgevingsschema waar deze typestrings vandaan komen.

Cache-Control is hoe u elke cache tussen uw server en de lezer beheert. public, max-age=31536000, immutable is geschikt voor assets waarvan de bestandsnaam een content-hash bevat, omdat de naam verandert wanneer de inhoud verandert. no-store hoort bij alles wat gebruikersspecifiek is, omdat een gedeelde cache die een ingelogde pagina bewaart, deze aan de volgende persoon zal tonen die om dezelfde URL vraagt. private is de tussenliggende instelling: de browser mag het bewaren, een gedeelde cache mag dat niet.

X-Forwarded-For bestaat omdat een proxy de bezoeker verbergt. Zodra een verzoek door een reverse proxy gaat, is $remote_addr het adres van de proxy, waardoor uw logs, uw geolocatie en uw rate limiting allemaal slechts één client zien. De proxy moet het oorspronkelijke adres doorgeven:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

De ontvangende server moet vervolgens worden verteld om dit te geloven, en precies worden verteld wie hij moet geloven:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

Vermeld alleen bereiken die u beheert. X-Forwarded-For is platte tekst die elke client kan sturen, dus set_real_ip_from 0.0.0.0/0; laat een bezoeker het adres kiezen dat u logt en het adres dat uw rate limiter telt.

X-Forwarded-Proto voorkomt een specifieke en zeer veelvoorkomende fout. Uw proxy beëindigt TLS en stuurt het verzoek via platte HTTP door naar de applicatie. De applicatie ziet een plat verzoek, besluit dat de bezoeker op HTTPS moet zitten en antwoordt met 301 https://example.com/. De browser volgt dit, de proxy beëindigt opnieuw TLS en stuurt weer platte HTTP door, en de lus herhaalt zich totdat de browser opgeeft met ERR_TOO_MANY_REDIRECTS. Het sturen van X-Forwarded-Proto: https vertelt de applicatie dat de bezoeker al op HTTPS zit, waardoor het stoppen met redirecten wordt afgedwongen.

HTTP/1.1 versus HTTP/2 versus HTTP/3: wat verandert er voor u

HTTP/1.1 is tekstgebaseerd en verwerkt één verzoek per keer per verbinding. Connection: keep-alive staat toe dat het volgende verzoek dezelfde TCP-verbinding hergebruikt, wat de opstartkosten bespaart, maar antwoorden komen nog steeds terug in de volgorde waarin ze werden aangevraagd. Eén traag antwoord blokkeert alles wat erachter in de wachtrij staat. Dit is head-of-line blocking, en browsers omzeilen dit door meerdere verbindingen tegelijk naar dezelfde hostnaam te openen.

HTTP/2 behoudt dezelfde methoden en statuscodes, maar wijzigt de framing naar binair. Meerdere verzoeken delen één verbinding als onafhankelijke streams, en herhaalde headertekst wordt gecomprimeerd. Dit is relevant omdat een modern verzoek veel headers bevat. De verbinding is nog steeds TCP, dus één verloren pakket vertraagt elke stream op die verbinding totdat de hertransmissie arriveert. De head-of-line blocking is niet verdwenen; deze is verplaatst van HTTP naar de transportlaag. Server push was onderdeel van HTTP/2, maar wordt in de praktijk niet meer gebruikt, omdat Chrome de ondersteuning hiervoor in 2022 heeft verwijderd.

HTTP/3 behoudt opnieuw dezelfde semantiek en vervangt TCP door QUIC, een transportprotocol gebouwd op UDP (user datagram protocol). QUIC-streams zijn volledig onafhankelijk, waardoor een verloren pakket alleen de stream vertraagt waartoe het behoorde. TLS 1.3 is ingebouwd in de QUIC-handshake in plaats van er bovenop geplaatst, waardoor een nieuwe verbinding minder round-trips vereist. Dit heeft twee praktische gevolgen: UDP-poort 443 moet openstaan in elke firewall op het pad, en elk netwerk dat UDP vertraagt of blokkeert, dwingt clients terug naar HTTP/2.

Wat verandert er concreet voor u? Browsers starten nooit direct op HTTP/3. Ze verbinden via HTTP/2 of HTTP/1.1, zien een Alt-Svc: h3=":443"; ma=86400-header in het antwoord en gebruiken HTTP/3 voor latere verbindingen met die host. De header is dus geen optionele versiering, maar het ontdekkingsmechanisme. In nginx werd HTTP/2 een eigen directive in versie 1.25.1 (http2 on; binnen het server-blok, ter vervanging van de oude listen ... http2-parameter), en QUIC werd geïntroduceerd in mainline 1.25.0, waarbij een HTTP/3-site listen 443 quic reuseport; vereist naast de gebruikelijke listen 443 ssl;.

Proxies verschillen in volwassenheid op dit gebied, en het is de moeite waard dit te controleren tegen de versie die u daadwerkelijk draait. Sinds augustus 2026 serveert Caddy standaard HTTP/3 zonder configuratie. nginx vereist de expliciete quic-listener plus de hierboven beschreven Alt-Svc-header. Traefik schakelt het per entry point in via een expliciete http3-optie. Als u TLS termineert bij Traefik voor meerdere Docker-applicaties, wordt de protocolversie die uw bezoekers krijgen daar bepaald, en de hop van de proxy naar uw container is meestal standaard HTTP/1.1, ongeacht wat de browser heeft onderhandeld.

Controleer het in plaats van aan te nemen. curl --http3 -sS -o /dev/null -D - https://example.com/ werkt alleen als curl -V HTTP3 in zijn functies vermeldt, en de meeste distributie-builds bevatten dit niet. De betrouwbare controle is uw eigen logboek: voeg $server_protocol toe aan het formaat en lees wat echte browsers onderhandelen. Bevestig voor dat alles of UDP 443 daadwerkelijk openstaat, want een firewall die alleen TCP 443 toestaat, zal HTTP/3 geruisloos laten falen terwijl de site blijft werken via HTTP/2. Weten welke poorten openstaan en luisteren op uw Linux-server is het eerste wat u moet controleren.

HTTPS: HTTP is het protocol, TLS is de verpakking

HTTPS is geen afzonderlijk protocol. Het zijn dezelfde verzoeken en dezelfde statuscodes die binnen een TLS-sessie worden verstuurd. Poort 80 verstuurt deze in platte tekst en poort 443 verstuurt ze versleuteld. De TLS-handshake wordt eerst voltooid, waarna het HTTP-verzoek door het versleutelde kanaal reist. Vanwege deze volgorde heeft een certificaatprobleem nooit een statuscode: de fout treedt op voordat er één HTTP-byte is verzonden, waardoor er geen respons is om te nummeren.

Eén detail in de volgorde is van belang bij een server die meerdere sites host. Het certificaat wordt gekozen met behulp van SNI (server name indication), een veld in de TLS-handshake dat de hostnaam in platte tekst doorgeeft voordat er een HTTP-header bestaat. De server kiest dus eerst een certificaat op basis van SNI en kiest vervolgens een virtual host op basis van de Host header. Dit zijn twee afzonderlijke opzoekingen die normaal gesproken overeenkomen. Wanneer dit niet het geval is, toont de browser een naamfout zoals NET::ERR_CERT_COMMON_NAME_INVALID en verzendt deze helemaal geen verzoek, omdat het certificaat van uw standaardserver werd aangeboden voor een naam die het niet dekt.

Voor een publieke site vraagt u een officieel certificaat aan en laat u dit automatisch vernieuwen. Certbot met Let's Encrypt op nginx schrijft de certificaatpaden naar uw serverblok en installeert de vernieuwingstimer voor u. Voor een hostnaam die geen enkele publieke autoriteit kan valideren, zoals een interne naam of een kaal IP-adres op uw eigen netwerk, is een self-signed certificaat op Ubuntu de eerlijke optie, zolang u accepteert dat elke client moet worden geïnstrueerd om dit te vertrouwen.

Zodra TLS werkt, stuurt u al het verkeer op poort 80 door naar poort 443:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Voeg Strict-Transport-Security pas toe wanneer u er zeker van bent. De header add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; instrueert browsers om platte HTTP voor die hostnaam gedurende twee jaar te weigeren, en zij volgen dit op vanuit hun eigen cache. Dit betekent dat het later verwijderen van de header dit niet ongedaan maakt. Begin met een max-age van enkele uren, bevestig dat elk subdomein daadwerkelijk op HTTPS draait en verhoog de waarde daarna pas.

FAQ

Wat is het verschil tussen HTTP en HTTPS?

HTTPS is HTTP dat wordt verzonden binnen een TLS-sessie (transport layer security). De methoden en statuscodes zijn identiek. Het verschil is dat de bytes tussen de client en het punt waar TLS wordt beëindigd versleuteld zijn, en dat de standaardpoort wijzigt van 80 naar 443. Omdat de TLS-handshake wordt voltooid voordat de eerste HTTP-byte wordt verzonden, levert een certificaatfout nooit een statuscode op. Daarom toont een browser bij een certificaatwaarschuwing een foutnaam zoals NET::ERR_CERT_COMMON_NAME_INVALID in plaats van een nummer zoals 403.

Waarom geeft mijn site een 502 Bad Gateway?

Een 502 van nginx betekent dat nginx geen bruikbaar antwoord kon krijgen van de upstream waarnaar het verkeer doorstuurt. Het verzoek van de bezoeker was dus in orde, maar er is een probleem achter nginx. Lees /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream betekent dat er niets luistert op het adres en de poort in proxy_pass; controleer dus of de applicatie draait en is gebonden aan de verwachte locatie. no live upstreams while connecting to upstream betekent dat elke server in het upstream-blok als 'down' is gemarkeerd na herhaaldelijke fouten. Vergelijk dit met 504 Gateway Timeout, wat betekent dat de upstream de verbinding wel accepteerde, maar vervolgens niet binnen proxy_read_timeout antwoordde.

Waarom toont mijn toegangslog voor elke bezoeker hetzelfde IP-adres?

Omdat $remote_addr het adres registreert dat de TCP-verbinding opende, en achter een reverse proxy of een content delivery network is dat adres de proxy. Het adres van de bezoeker komt in plaats daarvan binnen via de X-Forwarded-For-header. Stel proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; in op de proxy en configureer vervolgens op de ontvangende nginx set_real_ip_from met het adresbereik van de proxy en real_ip_header X-Forwarded-For;. Vermeld alleen bereiken die u beheert, omdat die header tekst is die elke client kan verzenden. Als u deze van het hele internet vertrouwt, kan een bezoeker zelf het adres kiezen dat u logt en waarop u rate limiting toepast.

Moet ik HTTP/2 of HTTP/3 inschakelen?

HTTP/2 is de moeite waard om in te schakelen, omdat het slechts één instructie vereist op een site die al TLS gebruikt. Het heft de verbindingslimiet per verzoek op, die een pagina met veel kleine bestanden anders traag maakt. HTTP/3 levert een kleinere en minder zekere winst op en kost u een open UDP-poort 443, plus een proxy-build met QUIC-ondersteuning. Onthoud dat browsers pas overschakelen naar HTTP/3 nadat ze een Alt-Svc-header op een eerder antwoord hebben gezien; zonder die header verandert er niets, ongeacht wat uw listen-regel aangeeft. Voeg $server_protocol toe aan uw logformaat en meet wat bezoekers daadwerkelijk onderhandelen voordat u hier tijd aan besteedt.

Wat betekent 403 Forbidden als het bestand wel bestaat?

Op een statische site is een 403 meestal een kwestie van bestandssysteemrechten in plaats van een HTTP-regel. open() ... failed (13: Permission denied) in /var/log/nginx/error.log betekent dat de nginx-workergebruiker het bestand niet kan lezen. Dit komt meestal doordat een bovenliggende map geen uitvoerrechten (execute permission) heeft voor anderen, in plaats van dat de bestandsmodus zelf onjuist is. directory index of ... is forbidden betekent dat het verzoek leidde naar een map zonder indexbestand terwijl autoindex is uitgeschakeld. Een expliciete deny-regel in het bijbehorende location-blok geeft ook een 403 terug; lees dat blok dus als het foutenlogboek niets vermeldt.

#http#https#web-server#headers#http3