HEAD requests extra snel afhandelen

De robots, crawlers of spiders van zoekmachines en link checkers kunnen een webserver belasten met soms wel honderden verzoeken per dag. Vooral als je PHP-webpagina’s dynamisch genereert met een database, kan dit dataverkeer ten koste gaan van echte bezoekers. In dit artikel beschrijf ik hoe je de schade kunt beperken door HEAD requests een speciale behandeling te geven.

Het Hypertext Transfer Protocol (HTTP) kent verschillende methods of methoden voor het afhandelen van een request of verzoek. De HTTP-methode GET wordt gebruikt bij het opvragen van een webpagina als internetgebruikers op een hyperlink klikken of een URL invoeren in de adresbalk van een webbrowser. De methode POST wordt gebruikt voor het verzenden van een formulier als je het attribuut method="post" gebruikt in de HTML-tags <form>...</form>. In PHP vind je de data die hierbij worden doorgegeven in bijvoorbeeld de superglobal arrays $_GET en $_POST.

Een minder bekende HTTP-methode is HEAD. Deze methode wordt gebruikt om uitsluitend de HTTP-headers van een bestand op te vragen. De HTTP-methode HEAD heeft vooral één belangrijke praktische functie: met een HEAD request kan een client controleren of een webpagina nog bestaat.

Vooral zoekmachines en link checkers bestoken een website soms wel honderden keren per dag met HEAD requests. Het aantal verzoeken neemt logischerwijs toe naarmate de site groter en populairder is. Door de HTTP-methode HEAD een speciale behandeling te geven, kun je het dataverkeer daarvoor drastisch verminderen. Je hoeft immers geen complete webpagina te verzenden en je hoeft dus evenmin de databaseserver te belasten als je content uit een database haalt. De naam HEAD is wat onduidelijk, want de HTTP-client is niet geïnteresseerd in de inhoud van de HTML-container <head>...</head>, maar uitsluitend in de HTTP-headers van een webpagina.

Snelle oplossing

De server-side include head-v1.inc.php toont een snelle en eenvoudige oplossing. De HTTP-methode van het huidige verzoek vind je bij PHP in de gereserveerde servervariabele $_SERVER['REQUEST_METHOD']. Aangezien het HTTP-protocol niet hoofdlettergevoelig is, wordt de methode voor de zekerheid omgezet in hoofdletters met de stringfunctie strtoupper(). Is de methode gelijk aan HEAD, dan verzend je HTTP-statuscode 200 voor OK en beëindig je het PHP-script met exit(0):

<?php # head-v1.inc.php
if (strtoupper($_SERVER['REQUEST_METHOD']) == 'HEAD') {
    
header('HTTP/1.1 200 OK');
    exit(
0);
}
?>

Vaak is alleen de aanroep van exit(0) voldoende. Als je namelijk voor de parameter status van exit() de integer 0 gebruikt, wordt het PHP-script foutloos beëindigd en zal de webserver veelal HTTP-statuscode 200 verzenden naar de client. Doordat de configuratie van de webserver hierbij echter nog roet in het eten kan gooien, is het zelf verzenden de header met de PHP-functie header() een iets degelijkere oplossing.

Oplossing voor HTTP 1.0 en HTTP 1.1

De eerste oplossing gaat uit van HTTP 1.1 uit 1999. De kans dat een client nu nog HTTP 1.0 uit 1996 gebruikt, is namelijk vrij klein. Helemaal uitgesloten is het echter niet. De variant in head-v2.inc.php toont hiervoor een veiligere oplossing. Hierin wordt alleen HTTP 1.1 gebruikt als het protocol in de servervariabele $_SERVER['SERVER_PROTOCOL'] gelijk is aan de string HTTP/1.1 (met een slash in plaats van een spatie). Zo niet, dan valt het PHP-script terug op HTTP 1.0:

<?php # head-v2.inc.php
if (strtoupper($_SERVER['REQUEST_METHOD']) == 'HEAD') {
    if (
strtoupper($_SERVER['SERVER_PROTOCOL']) == 'HTTP/1.1') {
        
header('HTTP/1.1 200 OK');
    } else {
        
header('HTTP/1.0 200 OK');
    }
    exit(
0);
}
?>

Eerst alle HTTP-headers verzenden

Let in beide gevallen op waar je de server-side include insluit. Een HEAD request moet namelijk worden beantwoord met precies dezelfde HTTP-headers als een gewoon verzoek met GET. In paragraaf 9.4, ‘HEAD’, van RFC 2616 voor de specificatie van HTTP 1.1 staat heel duidelijk: “The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request.” Stel je dus zelf HTTP-headers in met de PHP-functie header(), dan moet je pas daarna een eventueel HEAD request afhandelen. Voor een webpagina met HTML of XHTML (Extensible HTML) in de tekenset UTF-8 en voor content in het Nederlands gebruik je bijvoorbeeld:

// HTTP-header voor HTML of XHTML met de tekenset UTF-8
header('Content-Type: text/html; charset=UTF-8');
// HTTP-header voor content in het Nederlands
header('Content-Language: nl');
// Server-side include voor HEAD requests
require('head-v2.inc.php');

HEAD requests weigeren

Wil je om duistere redenen HEAD requests weigeren? Dan kan dat eventueel ook. Hiervoor heb je twee HTTP-headers nodig:

  1. Eerst verzend je HTTP-statuscode 405 voor de clientfout Method Not Allowed om te melden dat de HTTP-methode HEAD niet is toegestaan.
  2. Bij statuscode 405 moet je volgens de specificatie van HTTP 1.1 vervolgens met de HTTP-header Allow melden welke methoden wel zijn toegestaan. Gebruik bijvoorbeeld de header Allow: GET, POST (met een komma tussen de methoden) als je alleen de HTTP-methoden GET en POST wilt accepteren.

Een voorbeeld van deze ‘oplossing’ vind je in de server-side include head-v3.inc.php:

<?php # head-v3.inc.php
if (strtoupper($_SERVER['REQUEST_METHOD']) == 'HEAD') {
    
// De HTTP-methode HEAD is niet toegestaan
    
header('HTTP/1.1 405 Method Not Allowed');
    
// De HTTP-methoden GET en POST zijn wel toegestaan
    
header('Allow: GET, POST');
    
// PHP-script foutloos beëindigen
    
exit(0);
}
?>

Deze techniek wordt nogal eens misbruikt voor sitestatistieken, webvertising en partnerprogramma’s van webwinkels. In een poging een onderscheid te maken tussen echte bezoekers (die de methode GET of POST gebruiken) en andere clients (bijvoorbeeld zoekmachines en link checkers), wordt de methode HEAD geweigerd. Volgens de regels hoort dit dus niet: zodra je GET accepteert, moet je een verzoek met HEAD beantwoorden met dezelfde HTTP-headers.