PHP5から実装された関数でget_headersというのがある。
URL指定したらhttp headerを返してくれる関数で、 file_get_contentsなんかでページを読む前にhttpヘッダーを見て処理したりしたい時に最適な感じです。
マニュアル読んで実装に使ってたんだけど、稀に辺な挙動をするので悩んでたんだが、リダイレクト回りに罠があったようだ。
事の発端は自作のircbotに搭載してるurl解析機能。
urlを含む発言を監視してtitleを教えてくれる機能が付いてるのだけど、google spreadsheetsのURLでの挙動がどうも辺なので色々とdumpしてみてみた所。
(第二引き数に0以外を設定した場合のもの)
Array ( [0] => HTTP/1.0 302 Moved Temporarily [Content-Type] => Array ( [0] => text/html; charset=UTF-8 [1] => text/html; charset=UTF-8 [2] => text/html; charset=UTF-8 [3] => text/html; charset=UTF-8 ) [Location] => Array ( [0] => http://www.google.com/url?q=https%3A%2F%2Fspreadsheets.google.com%2Fccc%3Fkey%3Dxxxxxx... [1] => https://spreadsheets.google.com/ccc?key=xxxxxx... [2] => https://www.google.com/accounts/ServiceLogin?service=wise&passive=true&nui=1&continue=https%3A%2F%2Fspreadsheets.google.com%2Fccc%3Fkey%3Dxxxxxx... ) [Date] => Array ( [0] => Tue, 13 Apr 2010 10:59:27 GMT [1] => Tue, 13 Apr 2010 10:59:27 GMT [2] => Tue, 13 Apr 2010 10:59:28 GMT [3] => Tue, 13 Apr 2010 10:59:28 GMT ) [1] => HTTP/1.0 302 Found [Content-Length] => Array ( [0] => 302 [1] => 11464 ) [2] => HTTP/1.0 302 Moved Temporarily [3] => HTTP/1.0 200 OK [Cache-control] => no-cache, no-store [Pragma] => no-cache(長かったので色々と省略した。
他にも色んなパラメータが3つずつ、ないし4つずとか来てた。
最初、てっきりgoogle側が内部のリダイレクトだからhttpパラメータをまとめて送ってきてるとか、httpハッダーにそんなのが出来るような仕様があるんだなーと思ってRFCの仕様を読むという明後日の方向に走ってたんだけど当然ながらそんな事は書いてなかった。
試しにtelnetで1個1個リクエスト投げてみたら、1個1個ちゃんと個別に帰ってくる。
その時点で気付いたんだけど、どうやらget_headers関数がリダイレクト先まで解決して取得してくれてたみたい。
気付くのが遅い、、、
実装してたURL解析機能の方ではレスポンスは1個1個帰ってくる前提でget_heardersが301とか302だったらリダイレクトである旨を表示しつつ最終的な転送先URLまで再帰で辿って処理するようにしてたんだが、、、
(何ステップまでのリダイレクト許可とか、循環リダイレクトの判定とかも書いてた。
get_headers側で全部やってくれるなら言う事ない感じ。
さらに、最終的なURLに対してget_file_contents投げるようにしてたんだどget_file_contents側もリダイレクトの解決をした結果を返してくれるらしい。
いやー色々関数側でやってくれてて助かるなー、、、と思わされたのも束の間。
上に貼ったgoogle spreadsheetsの結果を見てもらえば分かるが、同じ名前のヘッダーがあった場合のみ配列として格納される為、どの結果がどのレスポンスで返されたものなのかプログラム側から直感的に分かりにくい。
(入った順に処理されたままの順序で出てくるので、見ようによっては分かるが、それを解析させるプログラムを作るのは無駄だ。 それに将来的に保証されるものではない。)
という事で、get_headersの第二引き数に値をセットしない場合に出てくる、受けとったヘッダーそのまま配列にしたデータを元に分かり易い形に整形する物を作った。
<?php function my_get_headers( $url ){ $headers = get_headers($url); if(!$headers){ return $headers; } $res = Array(); $c=-1; foreach( $headers as $h ){ if(strpos($h,'HTTP/')===0){ $res[++$c]['status-line']=$h; $res[ $c ]['status-code']=(int)strstr($h, ' '); }else{ $sep = strpos($h,': '); $res[ $c ][ strtolower( substr($h,0,$sep)) ] = substr($h,$sep+2); } } $res['count']=$c; $res['last-status']=$res[$c]['status-code']; return $res; } ?>
これで
Array
( [0] => Array
[status-line] => HTTP/1.0 302 Moved Temporarily
[status-code] => 302
[x-frame-options] => ALLOWALL
[content-type] => text/html; charset=UTF-8
[location] => http://www.google.com/url....
[date] => Tue, 13 Apr 2010 16:49:41 GMT
[expires] => Tue, 13 Apr 2010 16:49:41 GMT
....................
)
[1] => Array
(
[status-line] => HTTP/1.0 302 Found
[status-code] => 302
...........
[2] => Array ( ... )
[3] => Array ( ... )
[count] => 3
[last-status] => 200
みたいにレスポンス毎に整形された配列が取れるようになった。
ってか正直なんで最初っからこうなてないのか疑問に思う。
「1行ずつ処理して被るのがあったら配列にする」みたいな書き方、めんどうすぎると思うのだが、、、
ついでに連想配列の添字になるフィールド名を小文字に統一した。
本来はContent-Typeみたいな形で来るのがRFCの仕様に準拠した形なのだが、一部のサーバーでContent-typeになってる物があったりで、受けとる側で面倒臭い。
最後にレスポンスの件数と最終的なステータスを追加しといた。
1度のレスポンスで同じフィールド名の物が仕様上あるのか分からんのだけど、あったらこれだけじゃ問題があるかも。(後から来た物で上書きされる。
まぁ問題でたらそん時考える( ´-`)
もう一つ気になるのは循環リダイレクトの処理と多段リダイレクト時のpop数。
試しにサーバー上に自ファイルに飛ぶリダイレクト作ってみたらあっさり20段まで飛んでくれた。
自分自身に飛ぶの処理できてない時点で循環は見れてない。
とりあえず20段まで行ってたらlocationsの中にURL重複してるのがないか確認して、重複してたらループ表示、重複してなかったら20段までみたけど無かったよー って出すか。
これ何段までとか設定できないんかなぁ。
コメントする