Edited at

Mac の PHP はパーセントエンコーディングな日本語リダイレクトを解釈できない?

More than 3 years have passed since last update.

『Mac 環境の PHP だけ挙動が違う』という、首をかしげる問題に遭遇したのでメモ。


追記 (2016/06/23 00:28)

厳密には


  • OS 依存ではなく、ロケールに依存する

  • パーセントエンコーディングが引き金ではなく、日本語の文字コードに含まれる制御文字が引き金

という問題のようです。詳しくは下部の追記およびコメントをどうぞ。

@rryu さん ご教示ありがとうございます :bow:


Location ヘッダのパーセントエンコーディングが解釈できない

file_get_contents を使って URL の内容を取得する、なんてことはよくあります。楽ですし。

そこで指定した URL が 301 リダイレクトを返す場合、Location ヘッダにパーセントエンコーディングを含むと Invalid redirect URL! の Warning が出る 、という問題に遭遇しました。


例:Wikipedia の HTTP ページ

例えば、Wikipediaのトップページは https://ja.wikipedia.org/wiki/メインページ です。このプロトコルを http にすると、https への 301 リダイレクトになります。

で、日本語部分をパーセントエンコーディングして、"http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8" を file_get_contents() で取ってみます。

$ curl --head "http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"

HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
PHP Warning: file_get_contents(http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8):
failed to open stream: Invalid redirect URL! https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 in Command line code on line 1

PHP Stack trace:
PHP 1. {main}() Command line code:0
PHP 2. file_get_contents() Command line code:1

Invalid redirect URL! と怒られてしまいました。 (※ 上記は整形してます)

El Capitan に添付されている PHP 5.5.34 、および phpenv + php-build で導入した PHP 5.6.22、さらには現時点の最新の PHP 7.0.7 でもこの問題が発生します。

もちろんブラウザでは正しく閲覧できますし、curl -L ならちゃんとリダイレクトも追えます。


エンコーディングされていない Location ヘッダは?

パーセントエンコーディングされていない(日本語のままの)Wikipedia の URL を渡してあげると、Location ヘッダには そのまま日本語が入ってきます。

この場合、 file_get_contents() で Warning は 出ません。

$ curl --head "http://ja.wikipedia.org/wiki/メインページ"

HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/メインページ
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/メインページ');"
(正常終了)


Mac 固有の問題?

パーセントエンコーディングを含んだリダイレクトを検証するため、


  • ubuntu

  • Debian

  • CentOS

といった各種 Linux ディストリビューション向けにビルドされた PHP でも試してみました。

ところが、いずれの PHP もパーセントエンコーディングを正しく解釈し、Warning が出ずに正常終了 してしまいます。

$ curl --head "http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"

HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
(...正常終了!?)

…えっ、Mac 固有の問題なのこれ……!?


エラー発生箇所

PHP のソースで言うと、発生しているエラーはおそらくこのへん

間違ってました。詳しくはコメントをどうぞ。


回避策

もしこの問題に出会ってしまったら、 cURL 関数群 を使って回避するのが堅実でしょう。

file_get_contents は非常に楽ちんですが、curl は安心を買えます。

http://php.net/manual/ja/function.curl-exec.php#refsect1-function.curl-exec-examples

追記:file_get_contents のまま問題を回避することも可能ですが、ロケールへの依存を残すよりは やはり curl を素直に使った方が堅実です。


なぜ Mac だけ?

とはいえ、なぜ Mac の PHP だけで このような Warning が出るのか、正しい原因がまだ掴めてません。

PHP のバージョンや php.ini の設定も疑ったのですが、同一にしても Mac 環境だけはやはりエラーになります。

そもそも、確実に再現するのが Mac というだけで、『本当に Mac 固有の問題なのか』も微妙ですし、ちょっとモヤモヤしています。

この Warning の原因に関して、ご存知の方はご教示いただけると嬉しいです。 :bow:



追記:ご教示いただきました


コントロールコードかどうかの判定にiscntrl()関数を使っていますが、この関数の挙動はロケールに依存します。

ということでMacとそれ以外で環境変数''LC_*''系の値が異なるのではないかと思いますがどうでしょうか。


PHP のソースも読んだつもりでしたが、ここの部分だけ思いっきり見落としていました…。


追加検証

Warning が出た時の Mac のロケールは ja_JP.UTF-8 です。

$ locale

LANG="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_CTYPE="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_ALL=

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
PHP Warning: file_get_contents(http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8):
failed to open stream: Invalid redirect URL! https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 in Command line code on line 1

試しに LC_CTYPE=C に設定して同じコードを実行すると、Warning が出ず正常終了 しました。PHP コード内部でも setlocale() 関数を使用することで回避可能。

$ LC_CTYPE=C php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"

(正常終了)

$ php -r "setlocale(LC_CTYPE, 'C'); file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
(正常終了)


Mac だけ Warning になる真相

どの文字が iscntrl() で制御文字と判定されているのか調べてみました。以下の検証用コード使用。



検証用PHPコード

<?php

for ($i = 0; $i <= 255; $i++) {
if (ctype_cntrl($i)) printf('0x%02x ', $i);
}




  • Mac 10.11



    • LC_CTYPE=C0x000x1f 0x7f


    • LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f 0x800x9f




  • 自分が試した Linux ディストリビューション (ubuntu, Debian, CentOS)



    • LC_CTYPE=C0x000x1f 0x7f


    • LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f



Mac 環境『だけ』、日本語 UTF-8 の時は 0x800x9f が制御文字扱いになってます。

この範囲は UTF-8 の3バイト文字のうち、1バイト目が 0xed で始まる文字の 2バイト目 にあたります (RFC 3629 / UTF8-3)。

この範囲が、URL にある %82%83 とぶつかって、制御文字が含まれていると認識されてエラーになる訳ですね。1バイト目は %ED じゃないんだけどなぁ。。。


 


正しい回避策は?

仮に setlocale(LC_CTYPE, 'C') で回避したとしても ロケール依存問題は消えるわけじゃないので、やっぱり curl を素直に使うべきなんでしょうね…

逆に言えば、PHPコード内で setlocale() を使用してロケールを変更したり、PHP を動作させる環境のロケール設定が変わった場合(仮にLinuxだろうと)、想定せずこの Warning の影響を受ける可能性がある とも言えます。

 


追記2:私が本当に踏んだモノ (2016/06/24 23:00)

実は、この記事を書くきっかけになった「地雷」は、Mac 環境のコトじゃありません。CentOS です。


  • CentOS 6.8 環境で PHP が動作していた。

  • 使っていた PHP フレームワークの中で setlocale() を使用していた。


  • LC_CTYPE"ja_JP" を設定していた。

その結果、やはり例の文字コードが制御文字に。 :sob:



  • :o: LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f


  • :x: LC_CTYPE=ja_JP0x000x1f 0x7f 0x800x8d 0x900x9f

UTF-8 設定を勝手に使ってくれると思い込むものでは無いですね。

Mac 環境と異なり、0x8e 0x8f の2字は制御文字扱いに なりません。 これは UNIX 由来の EUC-JP に合わせられたものと考えられます(それぞれ、半角カナ・補助漢字で使用されるコードで、領域も ja_JP.eucjp と一致します)。


結論

要は、こんな混沌としたハマり方をしないためにも、素直に curl_exec 使っておきましょう。

http://php.net/manual/ja/curl.examples-basic.php