TOC

Tornado & HTTP 599

Tornado POST 请求直接 599 了,经过 WireShark 抓包发现根本没有发出请求。
最后竟然发现和版本有关,4.1 下才有问题,4.2 以后就好了。

实验代码

# -*- coding: utf-8 -*-

import logging
import urllib

from tornado import gen, httpclient, ioloop

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s [%(name)s:%(funcName)s#%(lineno)s] %(message)s')
LOG = logging.getLogger(__name__)

API_KEY = '5j1znBVAsnSf5xQyNQyq'
API_URL = 'http://www.pm25.in/api/querys/pm2_5.json'
DEFAULT_HTTP_PARAMS = {
    'allow_nonstandard_methods': True,
    'raise_error': False,
    'request_timeout': 5,
    'connect_timeout': 3,
}


@gen.coroutine
def test():
    client = httpclient.AsyncHTTPClient()

    method = 'POST'

    params = [('city', 'beijing'), ('token', API_KEY)]
    qs = urllib.urlencode(params)
    url = API_URL + '?' + qs

    LOG.debug('%s %s', method, url)
    resp = yield client.fetch(url, method='POST', **DEFAULT_HTTP_PARAMS)
    LOG.debug([resp.code, resp.reason, resp.headers, resp.body])


def main():
    ioloop.IOLoop.current().run_sync(test)


if __name__ == '__main__':
    main()

实验

Tornado 4.1

2015-10-04 15:39:05,020 DEBUG [__main__:test#32] POST http://www.pm25.in/api/querys/pm2_5.json?city=beijing&token=5j1znBVAsnSf5xQyNQyq
2015-10-04 15:39:10,023 DEBUG [__main__:test#34] [599, 'Unknown', {}, None]

Tornado 4.2

2015-10-04 15:39:50,204 DEBUG [__main__:test#32] POST http://www.pm25.in/api/querys/pm2_5.json?city=beijing&token=5j1znBVAsnSf5xQyNQyq
2015-10-04 15:39:50,603 DEBUG [__main__:test#34] [404, 'Not Found', {'Status': '404', 'Content-Length': '729', 'X-Powered-By': 'Phusion Passenger (mod_rails/mod_rack)', 'X-Request-Id': 'c5442b9e5f79ff1e834837f07f617572', 'Server': 'nginx', 'Connection': 'close', 'Date': 'Tue, 12 Oct 2021 07:39:50 GMT', 'X-Runtime': '0.013833', 'Content-Type': 'text/html; charset=utf-8', 'X-Rack-Cache': 'invalidate, pass'}, '<!DOCTYPE html>\n<html>\n<head>\n  <title>The page you were looking for doesn\'t exist (404)</title>\n  <style type="text/css">\n    body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }\n    div.dialog {\n      width: 25em;\n      padding: 0 4em;\n      margin: 30em auto 0 auto;\n      border: 1px solid #ccc;\n      border-right-color: #999;\n      border-bottom-color: #999;\n    }\n    h1 { font-size: 100%; color: #f00; line-height: 1.5em; }\n  </style>\n</head>\n\n<body>\n  <!-- This file lives in public/404.html -->\n  <div class="dialog">\n    <h1>The page you were looking for doesn\'t exist.</h1>\n    <p>You may have mistyped the address or the page may have moved.</p>\n  </div>\n</body>\n</html>\n']

结论

经过对源码的分析,发现是 4.1 版本发送请求的时候,如果没有传 body,不会调用 connection.finish,请求不会真实发出,直到超时被 Tornado 捕获。
这个问题在 2015/03/09 被修复(commit),然后并入了 Tornado 4.2。

commit cf2a54794ff5067d6d815013d6570ee10f74d5e5
Author: Ben Darnell <ben@bendarnell.com>
Date:   2015-03-08 23-08-25

    simple_httpclient: finish() should be called even for body-less GET.

    This matters for HTTP2 where there is an explicit end-stream marker.

diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py
index beb55f23..6321a81d 100644
--- a/tornado/simple_httpclient.py
+++ b/tornado/simple_httpclient.py
@@ -375,7 +375,6 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
     def _write_body(self, start_read):
         if self.request.body is not None:
             self.connection.write(self.request.body)
-            self.connection.finish()
         elif self.request.body_producer is not None:
             fut = self.request.body_producer(self.connection.write)
             if is_future(fut):
@@ -386,7 +385,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
                         self._read_response()
                 self.io_loop.add_future(fut, on_body_written)
                 return
-            self.connection.finish()
+        self.connection.finish()
         if start_read:
             self._read_response()

我认为这是一个 BUG,既然有参数 allow_nonstandard_methods 允许不带 body 的 POST 请求,就应该将请求发出。
不过这个提交日志好像我说的没有关系...

定位到了问题就知道该怎么修复了,只需要传一个 body='' 就行了。