urllib2でtimeoutを捕まえる

urllib2を使っていて、timeoutを入れたくなりました。で、timeoutが発生し たらリトライをする、といった処理をしたいのですが、このtimeoutがどうい う例外なのかが分かりませんでした。

世界のtk0miya先生は urlib2.URLErrorで、messageに "timed out"と書かれてい る例外が飛んでくる、とおっしゃっていました。

しかし、手元で試したところ、 socket.timeout が飛んできておりました。

はてな?

手元は2.7.2 on Linux(ubuntu)、世界の小宮先生は2.7.1とのこと。

書いていたのはこういうコードでした。

try:
    f = urllib2.urlopen(req, timeout=timeout)
    body = f.read()
except socket.timeout, e:
    print("TIMEOUT!")

コードを追う

ということで、 urllib2のコード を追っ ていきます。

def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
    global _opener
    if _opener is None:
        _opener = build_opener()
    return _opener.open(url, data, timeout)

でtimeoutを設定したのち、openしてます。実際のopenは このあたり です。

try:
    h.request(req.get_method(), req.get_selector(), req.data, headers)
except socket.error, err: # XXX what error?
    h.close()
    raise URLError(err)

ほー。socket.errorであればURLErrorで投げなおしているようです。であれば、 URLErrorがということ?

socketを追っていきます。

Lib/socket.py はcreate_connection()とwrite()、readinto()が呼ばれており、実態は Modules/socketmodule.c へ。

ここを見ると、

static PyObject *socket_timeout;

(略)

socket_timeout = PyErr_NewException("socket.timeout",
                                    socket_error, NULL);
Py_INCREF(socket_timeout);
PyModule_AddObject(m, "timeout", socket_timeout);

とあり、socket.timeoutという例外を作っています。baseクラスは socket_errorですね。

実際に投げているところはこんな感じでいろいろなところで投げています。

if (timeout == 1) {
    PyErr_SetString(socket_timeout, "timed out");
    return NULL;
}

ふむふむ。

基本に戻れ

と、ここで世界の小宮先生から「traceは?」との指摘が。確かに。

見てみるとこんな感じでした。

Traceback (most recent call last):
  File "hoge.py", line 62, in _callAPI
    body = f.read()
  File "/usr/lib/python2.7/socket.py", line 351, in read
    data = self._sock.recv(rbufsize)
  File "/usr/lib/python2.7/httplib.py", line 561, in read
    s = self.fp.read(amt)
  File "/usr/lib/python2.7/socket.py", line 380, in read
    data = self._sock.recv(left)
timeout: timed out

あ!urllib2を通っていない?

確認してみると世界の小宮先生の場合、 urlopen でtimeoutしているとのこと。 一方、今回はreadのところでtimeoutしています。

つまり、

  • urlopenでtimeoutの場合

    urllib2.URLError が飛んでくる

  • read()でtimeoutの場合

    socket.timeout が飛んでくる

のではないかと考えられます。

検証

socket.pyの readinto を見てみると、

if self._timeout_occurred:
    raise IOError("cannot read from timed out object")
while True:
    try:
         return self._sock.recv_into(b)
    except timeout:
        self._timeout_occurred = True
        raise
    except InterruptedError:
        continue
    except error as e:
        if e.args[0] in _blocking_errnos:
            return None
        raise

となっています。timeoutを捕まえてそのままraiseしています。ん?while trueに入る前にtimeoutが起きるとIOErrorが変えるのか。これは今は関係ないけどちょっと気になる点。

一方、 create_connection

for res in getaddrinfo(host, port, 0, SOCK_STREAM):
    af, socktype, proto, canonname, sa = res
    sock = None
    try:
        sock = socket(af, socktype, proto)
        if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
            sock.settimeout(timeout)
        if source_address:
            sock.bind(source_address)
        sock.connect(sa)
        return sock

    except error as _:
        err = _
        if sock is not None:
            sock.close()

if err is not None:
    raise err
else:
    raise error("getaddrinfo returns an empty list")

とあり、socket.errorを捕まえてそのまま投げています。

と、ここでtimeoutしていたら、世界の小宮先生が 続き を書いてしまわれました。

どこでこういう変更があったか

ということで、乗りかかった船で、どこでどういう変更があったかを追跡して みる。といっても、先生の後を追っているだけだけど。

で、調べてみると 71523: b66bbbdc7abd で urllib2 に 変更が あったみたいだ。

変更前

try:
    h.request(req.get_method(), req.get_selector(), req.data, headers)
    try:
        r = h.getresponse(buffering=True)
    except TypeError: #buffering kw not supported
        r = h.getresponse()
except socket.error, err: # XXX what error?
    raise URLError(err)
finally:
    h.close()

変更後

try:
    h.request(req.get_method(), req.get_selector(), req.data, headers)
except socket.error, err: # XXX what error?
    h.close()
    raise URLError(err)
else:
    try:
        r = h.getresponse(buffering=True)
    except TypeError: # buffering kw not supported
        r = h.getresponse()

b66bbbdc7abd以前であれば、request()でもgetresponse()でも、socket.errorは すべてURLErrorになって投げ直されています。

ところが、変更以後はh.request()で発生したsocket.errorはURLErrorとなり ますが、それ以降のh.getresponse()で発生したsocket.errorはそのまま上がっ ていくことになります。

h.request()はhttplib中でsocket.sendall()を呼んでおり、h.getresponse() はsocket.read()を呼んでいます。

h.request()でtimeout、つまり、sendでtimeoutすると、URLErrorとなります が、一旦送りきってしまえばURLErrorとはなりません。…ほんと?

なお、f.read()部分ではurllib2を通らず、httplib経由でsocket.pyを叩くこと になりますので、このURLErrorでの投げ直しは行われず、そのまま socket.errorになります。

小宮先生にも検証してもらって、確証が持てました。

というわけで、結論です。

timeout時に起きる例外

urlopen()中でHTTP Request送信中

urlopen()中でHTTPヘッダ受信中

f.read()でHTTP Body部受信中

Python2.7.1

urllib2.URLError

urllib2.URLError

socket.timeout

Python2.7.2

urllib2.URLError

urllib2.URLError

socket.timeout

Python2.7.1

urllib2.URLError

socket.timeout

socket.timeout

とはいえ、結局はurllib2.URLErrorとsocket.timeoutの両方を捕まえておけば いい、と言うコードになるわけですが。

Comments

comments powered by Disqus