HTTP response headers & PHP stream wrappers
Van het weekend was ik aan het spelen met de Twitter API en liep, ondanks een caching constructie, op een gegeven moment toch tegen het rate limit aan; je mag vanwege de performance problemen eerder dit jaar momenteel max. 20 requests per uur naar de Twitter API doen.
De Twitter API geeft dit aan met een HTTP 400 response code. Ik kon dit alleen niet goed detecteren want fopen() retourneert alleen maar FALSE en ik wilde graag de daadwerkelijke HTTP response code hebben.
Het feit dat je PHP dan toch al behoorlijk wat jaartjes gebruikt houdt niet in dat PHP dan ook geen verrassingen meer voor je heeft. Sowieso aangezien er behoorlijk wat toegevoegd wordt aan PHP, maar ook omdat er soms gewoon rare, verstopte, dingen in PHP zitten. Het detecteren van de HTTP response headers bij een mislukt HTTP request is daar wat mij betreft een van.
Het probleem met fopen() en stream wrappers
Laten we er even wat code bij pakken. Hieronder volgt een manier om de REST API van Twitter aan te spreken. Je krijgt hiermee, mits je de juiste gegevens invult, een lijst van de mensen die jou volgen in JSON formaat:
-
class Twitter_Client {
-
/* ... */
-
-
private function doRequest($location, $action) {
-
$actionURL =
-
. '@twitter.com/' . $location . '/' . $action . '.json';
-
-
-
if (!$fp) {
-
/* Vis de inlog gegevens uit de URL voordat we de exceptie gooien. Die gaan niemand verder wat aan. */
-
throw new HTTP_Exception("Could not access URL: $url");
-
}
-
-
$data = '';
-
-
-
return $data;
-
}
-
-
public function getFollowers() {
-
return $this->doRequest('statusses', 'followers');
-
}
-
-
/* ... */
-
}
Note: Je hebt misschien opgemerkt dat ik hier geen timeout handling implementeer en dat is met de Twitter API best wel belangrijk gezien Twitter's uptime record :P. Dit heb ik gedaan om de code kort en duidelijk te houden. Elders op Scriptorama kun je leren hoe je timeouts met PHP streams kunt afhandelen.
Je kunt in dit voorbeeld, als het HTTP request lukt, eventueel de HTTP request headers achterhalen met de functie stream_get_meta_data(). Maar op het moment dat het request niet lukt, doordat je bijvoorbeeld een niet bestaande actie aanroept (HTTP 404) of je hebt meer requests in een uur gedaan dan toegestaan (HTTP 400), dan zal fopen() FALSE terug geven.
Helaas kun je stream_get_meta_data() dan niet meer gebruiken omdat deze een valide stream resource verwacht en die hebben we niet gekregen van fopen().
Een oplossing is om fopen() niet meer te gebruiken en gebruik te maken van fsockopen() en het HTTP protocol zelf dan maar enigzins te implementeren. Dat geeft je directe toegang tot alle headers. Dat kan, maar daarmee gooi je een hoop functionaliteit (redirects, het combineren van andere stream wrappers) de deur uit. Niet een goed idee dus.
Het is (gelukkig) mogelijk om er achter te komen wat er nou precies gebeurd is. Het zit alleen een beetje verstopt.... and it ain't pretty....
De oplossing: $http_response_header
Wat PHP namelijk doet bij het gebruik van de HTTP stream-wrapper via bijv. fopen() is een variabele aanmaken genaamd $http_response_header. Dat lees je goed, PHP maakt in je huidige scope een nieuwe variabele aan. How's that for a WTF-factor?
Deze variabele, een array, bevat alle HTTP response headers als element:
-
array(15) {
-
[0]=>
-
string(24) "HTTP/1.1 400 Bad Request"
-
[1]=>
-
string(35) "Date: Sun, 22 Jun 2008 09:43:47 GMT"
-
[2]=>
-
string(10) "Server: hi"
-
[3]=>
-
string(23) "Status: 400 Bad Request"
-
[4]=>
-
string(43) "P3P: CP="NOI DSP COR NID ADMa OPTa OUR NOR""
-
[5]=>
-
string(18) "X-Runtime: 0.01620"
-
[6]=>
-
string(36) "Cache-Control: no-cache, max-age=300"
-
[7]=>
-
string(45) "Content-Type: application/json; charset=utf-8"
-
[8]=>
-
string(19) "Content-Length: 124"
-
[9]=>
-
string(27) "Set-Cookie: lang=en; path=/"
-
[10]=>
-
string(27) "Set-Cookie: lang=en; path=/"
-
[11]=>
-
string(259) "Set-Cookie: _twitter_sess=d4th4dj3g3dr00md; domain=.twitter.com; path=/"
-
[12]=>
-
string(27) "Via: 1.0 web067.twitter.com"
-
[13]=>
-
string(38) "Expires: Sun, 22 Jun 2008 09:48:47 GMT"
-
[14]=>
-
string(17) "Connection: close"
-
}
Met een simpele regular expression kunnen we dus de HTTP response code uit het eerste element van $http_response_header halen:
-
if (!$fp) {
-
-
if (isset($http_response_header[0]) && preg_match('~^HTTP/1.[01] ([0-9]{3}) (.*)$~i', $http_response_header[0], $matches))
-
{
-
if ($matches[1] == '400')
-
throw new Twitter_Exception('Twitter Rate limit exceeded');
-
else
-
throw new HTTP_Exception("Could not open URL: $url ($matches[1] - $matches[2])");
-
} else
-
throw new HTTP_Exception("Could not open URL: $url");
-
}
Conclusie
De magische variabele $http_response_header kan je helpen bij het verwerken van HTTP requests die niet helemaal gaan zoals je misschien verwacht had.
Zoals je misschien gemerkt hebt vind ik het niet echt een knap systeem. Een variabele die ineens bestaat is een grote WTF-factor wat mij betreft. Zelf had ik eigenlijk verwacht dat dergelijke informatie verwerkt zou worden in bijvoorbeeld een stream context (voorbeeld van het gebruik van een stream context). Deze wordt momenteel alleen gebruikt om een stream te configureren, waarom ook niet gebruiken om informatie over het request in op te slaan? De naam context is er breed genoeg voor.
Volg Scriptorama via RSS!
Reageer ook!
wtf?
Door berry__
op 06.23.08 @ 8:54 am | Permalink
* kuch cURL kuch *
Door Sebastiaan Stok
op 06.23.08 @ 10:04 am | Permalink
@Sebastiaan: php stream wrappers zijn, imho, een stuk flexibeler:)
Door Mathieu Kooiman
op 06.23.08 @ 10:16 am | Permalink
Het staat zowaar in de manual ($http_response_header en HTTP wrapper ), maar ik heb deze vieze automagische var ook nooit eerder gespot. :P
Door Maarten
op 06.23.08 @ 11:39 am | Permalink
Dit gaat me helaas allemaal ver boven m'n pet ;-) Als ik zulke dingen lees denk ik weer 'ik ben gewoon een simpele php-programmeur'. Laat ik er maar gewoon voor zorgen dat de dingen werken, dan laat ik dit dieptewerk wel aan mensen zoals jullie over ;)
Door Edwin
op 06.23.08 @ 12:01 pm | Permalink
Klopt maar voor HTTP is het wel een stuk makkelijker ;)
Door Sebastiaan Stok
op 06.23.08 @ 7:38 pm | Permalink
Leuk als de url een location header bevat. Php volgt deze op en zo kun je in een oneindige loop belanden. Je bent dus afhankelijk van de security van de derde partij voor de werking van je eigen site. Beter is, zoals Sebastiaan voorstelt, om cURL te gebruiken. Hier kun je zulke dingen uitzetten (o.a. CURLOPT_FOLLOWLOCATION) en ben je imho stukken beter bezig.
Door Jochem Blok
op 06.25.08 @ 9:58 am | Permalink
Tikkeltje kort door de bocht, Jochem. Je kunt in een stream context de optie 'max_redirects' op 0 zetten om redirects te voorkomen, mocht je dat willen.
Door Mathieu Kooiman
op 06.25.08 @ 10:05 am | Permalink
Mathieu,
ik ben bezig met het schrijven van een script dat de twitter friends_timeline moet laten zien.
ik heb nu hetvolgende scriptje geschreven:
$url = "http://login:wachtwoord@twitter.com/statuses/friends_timeline.xml?count=3";
$fp = fopen($url, 'r');
if (!$fp) {
throw new HTTP_Exception("Could not access URL:". $url);
echo "jammer";
}
$data = fread($fp, filesize($fp));
fclose($fp);
echo $data;
de url is gebasseerd op uw script hierboven.
Jammer genoeg werkt dit niet...
hebt u enig id?
alvast bedankt!
Door Gert Poppe
op 07.28.08 @ 3:48 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>