httpclient_test.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. # -*- coding: utf-8 -*-
  2. import base64
  3. import binascii
  4. from contextlib import closing
  5. import copy
  6. import threading
  7. import datetime
  8. from io import BytesIO
  9. import subprocess
  10. import sys
  11. import time
  12. import typing # noqa: F401
  13. import unicodedata
  14. import unittest
  15. from tornado.escape import utf8, native_str, to_unicode
  16. from tornado import gen
  17. from tornado.httpclient import (
  18. HTTPRequest,
  19. HTTPResponse,
  20. _RequestProxy,
  21. HTTPError,
  22. HTTPClient,
  23. )
  24. from tornado.httpserver import HTTPServer
  25. from tornado.ioloop import IOLoop
  26. from tornado.iostream import IOStream
  27. from tornado.log import gen_log, app_log
  28. from tornado import netutil
  29. from tornado.testing import AsyncHTTPTestCase, bind_unused_port, gen_test, ExpectLog
  30. from tornado.test.util import skipOnTravis
  31. from tornado.web import Application, RequestHandler, url
  32. from tornado.httputil import format_timestamp, HTTPHeaders
  33. class HelloWorldHandler(RequestHandler):
  34. def get(self):
  35. name = self.get_argument("name", "world")
  36. self.set_header("Content-Type", "text/plain")
  37. self.finish("Hello %s!" % name)
  38. class PostHandler(RequestHandler):
  39. def post(self):
  40. self.finish(
  41. "Post arg1: %s, arg2: %s"
  42. % (self.get_argument("arg1"), self.get_argument("arg2"))
  43. )
  44. class PutHandler(RequestHandler):
  45. def put(self):
  46. self.write("Put body: ")
  47. self.write(self.request.body)
  48. class RedirectHandler(RequestHandler):
  49. def prepare(self):
  50. self.write("redirects can have bodies too")
  51. self.redirect(
  52. self.get_argument("url"), status=int(self.get_argument("status", "302"))
  53. )
  54. class RedirectWithoutLocationHandler(RequestHandler):
  55. def prepare(self):
  56. # For testing error handling of a redirect with no location header.
  57. self.set_status(301)
  58. self.finish()
  59. class ChunkHandler(RequestHandler):
  60. @gen.coroutine
  61. def get(self):
  62. self.write("asdf")
  63. self.flush()
  64. # Wait a bit to ensure the chunks are sent and received separately.
  65. yield gen.sleep(0.01)
  66. self.write("qwer")
  67. class AuthHandler(RequestHandler):
  68. def get(self):
  69. self.finish(self.request.headers["Authorization"])
  70. class CountdownHandler(RequestHandler):
  71. def get(self, count):
  72. count = int(count)
  73. if count > 0:
  74. self.redirect(self.reverse_url("countdown", count - 1))
  75. else:
  76. self.write("Zero")
  77. class EchoPostHandler(RequestHandler):
  78. def post(self):
  79. self.write(self.request.body)
  80. class UserAgentHandler(RequestHandler):
  81. def get(self):
  82. self.write(self.request.headers.get("User-Agent", "User agent not set"))
  83. class ContentLength304Handler(RequestHandler):
  84. def get(self):
  85. self.set_status(304)
  86. self.set_header("Content-Length", 42)
  87. def _clear_headers_for_304(self):
  88. # Tornado strips content-length from 304 responses, but here we
  89. # want to simulate servers that include the headers anyway.
  90. pass
  91. class PatchHandler(RequestHandler):
  92. def patch(self):
  93. "Return the request payload - so we can check it is being kept"
  94. self.write(self.request.body)
  95. class AllMethodsHandler(RequestHandler):
  96. SUPPORTED_METHODS = RequestHandler.SUPPORTED_METHODS + ("OTHER",) # type: ignore
  97. def method(self):
  98. self.write(self.request.method)
  99. get = head = post = put = delete = options = patch = other = method # type: ignore
  100. class SetHeaderHandler(RequestHandler):
  101. def get(self):
  102. # Use get_arguments for keys to get strings, but
  103. # request.arguments for values to get bytes.
  104. for k, v in zip(self.get_arguments("k"), self.request.arguments["v"]):
  105. self.set_header(k, v)
  106. # These tests end up getting run redundantly: once here with the default
  107. # HTTPClient implementation, and then again in each implementation's own
  108. # test suite.
  109. class HTTPClientCommonTestCase(AsyncHTTPTestCase):
  110. def get_app(self):
  111. return Application(
  112. [
  113. url("/hello", HelloWorldHandler),
  114. url("/post", PostHandler),
  115. url("/put", PutHandler),
  116. url("/redirect", RedirectHandler),
  117. url("/redirect_without_location", RedirectWithoutLocationHandler),
  118. url("/chunk", ChunkHandler),
  119. url("/auth", AuthHandler),
  120. url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
  121. url("/echopost", EchoPostHandler),
  122. url("/user_agent", UserAgentHandler),
  123. url("/304_with_content_length", ContentLength304Handler),
  124. url("/all_methods", AllMethodsHandler),
  125. url("/patch", PatchHandler),
  126. url("/set_header", SetHeaderHandler),
  127. ],
  128. gzip=True,
  129. )
  130. def test_patch_receives_payload(self):
  131. body = b"some patch data"
  132. response = self.fetch("/patch", method="PATCH", body=body)
  133. self.assertEqual(response.code, 200)
  134. self.assertEqual(response.body, body)
  135. @skipOnTravis
  136. def test_hello_world(self):
  137. response = self.fetch("/hello")
  138. self.assertEqual(response.code, 200)
  139. self.assertEqual(response.headers["Content-Type"], "text/plain")
  140. self.assertEqual(response.body, b"Hello world!")
  141. self.assertEqual(int(response.request_time), 0)
  142. response = self.fetch("/hello?name=Ben")
  143. self.assertEqual(response.body, b"Hello Ben!")
  144. def test_streaming_callback(self):
  145. # streaming_callback is also tested in test_chunked
  146. chunks = [] # type: typing.List[bytes]
  147. response = self.fetch("/hello", streaming_callback=chunks.append)
  148. # with streaming_callback, data goes to the callback and not response.body
  149. self.assertEqual(chunks, [b"Hello world!"])
  150. self.assertFalse(response.body)
  151. def test_post(self):
  152. response = self.fetch("/post", method="POST", body="arg1=foo&arg2=bar")
  153. self.assertEqual(response.code, 200)
  154. self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
  155. def test_chunked(self):
  156. response = self.fetch("/chunk")
  157. self.assertEqual(response.body, b"asdfqwer")
  158. chunks = [] # type: typing.List[bytes]
  159. response = self.fetch("/chunk", streaming_callback=chunks.append)
  160. self.assertEqual(chunks, [b"asdf", b"qwer"])
  161. self.assertFalse(response.body)
  162. def test_chunked_close(self):
  163. # test case in which chunks spread read-callback processing
  164. # over several ioloop iterations, but the connection is already closed.
  165. sock, port = bind_unused_port()
  166. with closing(sock):
  167. @gen.coroutine
  168. def accept_callback(conn, address):
  169. # fake an HTTP server using chunked encoding where the final chunks
  170. # and connection close all happen at once
  171. stream = IOStream(conn)
  172. request_data = yield stream.read_until(b"\r\n\r\n")
  173. if b"HTTP/1." not in request_data:
  174. self.skipTest("requires HTTP/1.x")
  175. yield stream.write(
  176. b"""\
  177. HTTP/1.1 200 OK
  178. Transfer-Encoding: chunked
  179. 1
  180. 1
  181. 1
  182. 2
  183. 0
  184. """.replace(
  185. b"\n", b"\r\n"
  186. )
  187. )
  188. stream.close()
  189. netutil.add_accept_handler(sock, accept_callback) # type: ignore
  190. resp = self.fetch("http://127.0.0.1:%d/" % port)
  191. resp.rethrow()
  192. self.assertEqual(resp.body, b"12")
  193. self.io_loop.remove_handler(sock.fileno())
  194. def test_basic_auth(self):
  195. # This test data appears in section 2 of RFC 7617.
  196. self.assertEqual(
  197. self.fetch(
  198. "/auth", auth_username="Aladdin", auth_password="open sesame"
  199. ).body,
  200. b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
  201. )
  202. def test_basic_auth_explicit_mode(self):
  203. self.assertEqual(
  204. self.fetch(
  205. "/auth",
  206. auth_username="Aladdin",
  207. auth_password="open sesame",
  208. auth_mode="basic",
  209. ).body,
  210. b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
  211. )
  212. def test_basic_auth_unicode(self):
  213. # This test data appears in section 2.1 of RFC 7617.
  214. self.assertEqual(
  215. self.fetch("/auth", auth_username="test", auth_password="123£").body,
  216. b"Basic dGVzdDoxMjPCow==",
  217. )
  218. # The standard mandates NFC. Give it a decomposed username
  219. # and ensure it is normalized to composed form.
  220. username = unicodedata.normalize("NFD", u"josé")
  221. self.assertEqual(
  222. self.fetch("/auth", auth_username=username, auth_password="səcrət").body,
  223. b"Basic am9zw6k6c8mZY3LJmXQ=",
  224. )
  225. def test_unsupported_auth_mode(self):
  226. # curl and simple clients handle errors a bit differently; the
  227. # important thing is that they don't fall back to basic auth
  228. # on an unknown mode.
  229. with ExpectLog(gen_log, "uncaught exception", required=False):
  230. with self.assertRaises((ValueError, HTTPError)):
  231. self.fetch(
  232. "/auth",
  233. auth_username="Aladdin",
  234. auth_password="open sesame",
  235. auth_mode="asdf",
  236. raise_error=True,
  237. )
  238. def test_follow_redirect(self):
  239. response = self.fetch("/countdown/2", follow_redirects=False)
  240. self.assertEqual(302, response.code)
  241. self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
  242. response = self.fetch("/countdown/2")
  243. self.assertEqual(200, response.code)
  244. self.assertTrue(response.effective_url.endswith("/countdown/0"))
  245. self.assertEqual(b"Zero", response.body)
  246. def test_redirect_without_location(self):
  247. response = self.fetch("/redirect_without_location", follow_redirects=True)
  248. # If there is no location header, the redirect response should
  249. # just be returned as-is. (This should arguably raise an
  250. # error, but libcurl doesn't treat this as an error, so we
  251. # don't either).
  252. self.assertEqual(301, response.code)
  253. def test_redirect_put_with_body(self):
  254. response = self.fetch(
  255. "/redirect?url=/put&status=307", method="PUT", body="hello"
  256. )
  257. self.assertEqual(response.body, b"Put body: hello")
  258. def test_redirect_put_without_body(self):
  259. # This "without body" edge case is similar to what happens with body_producer.
  260. response = self.fetch(
  261. "/redirect?url=/put&status=307",
  262. method="PUT",
  263. allow_nonstandard_methods=True,
  264. )
  265. self.assertEqual(response.body, b"Put body: ")
  266. def test_method_after_redirect(self):
  267. # Legacy redirect codes (301, 302) convert POST requests to GET.
  268. for status in [301, 302, 303]:
  269. url = "/redirect?url=/all_methods&status=%d" % status
  270. resp = self.fetch(url, method="POST", body=b"")
  271. self.assertEqual(b"GET", resp.body)
  272. # Other methods are left alone.
  273. for method in ["GET", "OPTIONS", "PUT", "DELETE"]:
  274. resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
  275. self.assertEqual(utf8(method), resp.body)
  276. # HEAD is different so check it separately.
  277. resp = self.fetch(url, method="HEAD")
  278. self.assertEqual(200, resp.code)
  279. self.assertEqual(b"", resp.body)
  280. # Newer redirects always preserve the original method.
  281. for status in [307, 308]:
  282. url = "/redirect?url=/all_methods&status=307"
  283. for method in ["GET", "OPTIONS", "POST", "PUT", "DELETE"]:
  284. resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
  285. self.assertEqual(method, to_unicode(resp.body))
  286. resp = self.fetch(url, method="HEAD")
  287. self.assertEqual(200, resp.code)
  288. self.assertEqual(b"", resp.body)
  289. def test_credentials_in_url(self):
  290. url = self.get_url("/auth").replace("http://", "http://me:secret@")
  291. response = self.fetch(url)
  292. self.assertEqual(b"Basic " + base64.b64encode(b"me:secret"), response.body)
  293. def test_body_encoding(self):
  294. unicode_body = u"\xe9"
  295. byte_body = binascii.a2b_hex(b"e9")
  296. # unicode string in body gets converted to utf8
  297. response = self.fetch(
  298. "/echopost",
  299. method="POST",
  300. body=unicode_body,
  301. headers={"Content-Type": "application/blah"},
  302. )
  303. self.assertEqual(response.headers["Content-Length"], "2")
  304. self.assertEqual(response.body, utf8(unicode_body))
  305. # byte strings pass through directly
  306. response = self.fetch(
  307. "/echopost",
  308. method="POST",
  309. body=byte_body,
  310. headers={"Content-Type": "application/blah"},
  311. )
  312. self.assertEqual(response.headers["Content-Length"], "1")
  313. self.assertEqual(response.body, byte_body)
  314. # Mixing unicode in headers and byte string bodies shouldn't
  315. # break anything
  316. response = self.fetch(
  317. "/echopost",
  318. method="POST",
  319. body=byte_body,
  320. headers={"Content-Type": "application/blah"},
  321. user_agent=u"foo",
  322. )
  323. self.assertEqual(response.headers["Content-Length"], "1")
  324. self.assertEqual(response.body, byte_body)
  325. def test_types(self):
  326. response = self.fetch("/hello")
  327. self.assertEqual(type(response.body), bytes)
  328. self.assertEqual(type(response.headers["Content-Type"]), str)
  329. self.assertEqual(type(response.code), int)
  330. self.assertEqual(type(response.effective_url), str)
  331. def test_header_callback(self):
  332. first_line = []
  333. headers = {}
  334. chunks = []
  335. def header_callback(header_line):
  336. if header_line.startswith("HTTP/1.1 101"):
  337. # Upgrading to HTTP/2
  338. pass
  339. elif header_line.startswith("HTTP/"):
  340. first_line.append(header_line)
  341. elif header_line != "\r\n":
  342. k, v = header_line.split(":", 1)
  343. headers[k.lower()] = v.strip()
  344. def streaming_callback(chunk):
  345. # All header callbacks are run before any streaming callbacks,
  346. # so the header data is available to process the data as it
  347. # comes in.
  348. self.assertEqual(headers["content-type"], "text/html; charset=UTF-8")
  349. chunks.append(chunk)
  350. self.fetch(
  351. "/chunk",
  352. header_callback=header_callback,
  353. streaming_callback=streaming_callback,
  354. )
  355. self.assertEqual(len(first_line), 1, first_line)
  356. self.assertRegexpMatches(first_line[0], "HTTP/[0-9]\\.[0-9] 200.*\r\n")
  357. self.assertEqual(chunks, [b"asdf", b"qwer"])
  358. @gen_test
  359. def test_configure_defaults(self):
  360. defaults = dict(user_agent="TestDefaultUserAgent", allow_ipv6=False)
  361. # Construct a new instance of the configured client class
  362. client = self.http_client.__class__(force_instance=True, defaults=defaults)
  363. try:
  364. response = yield client.fetch(self.get_url("/user_agent"))
  365. self.assertEqual(response.body, b"TestDefaultUserAgent")
  366. finally:
  367. client.close()
  368. def test_header_types(self):
  369. # Header values may be passed as character or utf8 byte strings,
  370. # in a plain dictionary or an HTTPHeaders object.
  371. # Keys must always be the native str type.
  372. # All combinations should have the same results on the wire.
  373. for value in [u"MyUserAgent", b"MyUserAgent"]:
  374. for container in [dict, HTTPHeaders]:
  375. headers = container()
  376. headers["User-Agent"] = value
  377. resp = self.fetch("/user_agent", headers=headers)
  378. self.assertEqual(
  379. resp.body,
  380. b"MyUserAgent",
  381. "response=%r, value=%r, container=%r"
  382. % (resp.body, value, container),
  383. )
  384. def test_multi_line_headers(self):
  385. # Multi-line http headers are rare but rfc-allowed
  386. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
  387. sock, port = bind_unused_port()
  388. with closing(sock):
  389. @gen.coroutine
  390. def accept_callback(conn, address):
  391. stream = IOStream(conn)
  392. request_data = yield stream.read_until(b"\r\n\r\n")
  393. if b"HTTP/1." not in request_data:
  394. self.skipTest("requires HTTP/1.x")
  395. yield stream.write(
  396. b"""\
  397. HTTP/1.1 200 OK
  398. X-XSS-Protection: 1;
  399. \tmode=block
  400. """.replace(
  401. b"\n", b"\r\n"
  402. )
  403. )
  404. stream.close()
  405. netutil.add_accept_handler(sock, accept_callback) # type: ignore
  406. resp = self.fetch("http://127.0.0.1:%d/" % port)
  407. resp.rethrow()
  408. self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
  409. self.io_loop.remove_handler(sock.fileno())
  410. def test_304_with_content_length(self):
  411. # According to the spec 304 responses SHOULD NOT include
  412. # Content-Length or other entity headers, but some servers do it
  413. # anyway.
  414. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
  415. response = self.fetch("/304_with_content_length")
  416. self.assertEqual(response.code, 304)
  417. self.assertEqual(response.headers["Content-Length"], "42")
  418. @gen_test
  419. def test_future_interface(self):
  420. response = yield self.http_client.fetch(self.get_url("/hello"))
  421. self.assertEqual(response.body, b"Hello world!")
  422. @gen_test
  423. def test_future_http_error(self):
  424. with self.assertRaises(HTTPError) as context:
  425. yield self.http_client.fetch(self.get_url("/notfound"))
  426. self.assertEqual(context.exception.code, 404)
  427. self.assertEqual(context.exception.response.code, 404)
  428. @gen_test
  429. def test_future_http_error_no_raise(self):
  430. response = yield self.http_client.fetch(
  431. self.get_url("/notfound"), raise_error=False
  432. )
  433. self.assertEqual(response.code, 404)
  434. @gen_test
  435. def test_reuse_request_from_response(self):
  436. # The response.request attribute should be an HTTPRequest, not
  437. # a _RequestProxy.
  438. # This test uses self.http_client.fetch because self.fetch calls
  439. # self.get_url on the input unconditionally.
  440. url = self.get_url("/hello")
  441. response = yield self.http_client.fetch(url)
  442. self.assertEqual(response.request.url, url)
  443. self.assertTrue(isinstance(response.request, HTTPRequest))
  444. response2 = yield self.http_client.fetch(response.request)
  445. self.assertEqual(response2.body, b"Hello world!")
  446. @gen_test
  447. def test_bind_source_ip(self):
  448. url = self.get_url("/hello")
  449. request = HTTPRequest(url, network_interface="127.0.0.1")
  450. response = yield self.http_client.fetch(request)
  451. self.assertEqual(response.code, 200)
  452. with self.assertRaises((ValueError, HTTPError)) as context:
  453. request = HTTPRequest(url, network_interface="not-interface-or-ip")
  454. yield self.http_client.fetch(request)
  455. self.assertIn("not-interface-or-ip", str(context.exception))
  456. def test_all_methods(self):
  457. for method in ["GET", "DELETE", "OPTIONS"]:
  458. response = self.fetch("/all_methods", method=method)
  459. self.assertEqual(response.body, utf8(method))
  460. for method in ["POST", "PUT", "PATCH"]:
  461. response = self.fetch("/all_methods", method=method, body=b"")
  462. self.assertEqual(response.body, utf8(method))
  463. response = self.fetch("/all_methods", method="HEAD")
  464. self.assertEqual(response.body, b"")
  465. response = self.fetch(
  466. "/all_methods", method="OTHER", allow_nonstandard_methods=True
  467. )
  468. self.assertEqual(response.body, b"OTHER")
  469. def test_body_sanity_checks(self):
  470. # These methods require a body.
  471. for method in ("POST", "PUT", "PATCH"):
  472. with self.assertRaises(ValueError) as context:
  473. self.fetch("/all_methods", method=method, raise_error=True)
  474. self.assertIn("must not be None", str(context.exception))
  475. resp = self.fetch(
  476. "/all_methods", method=method, allow_nonstandard_methods=True
  477. )
  478. self.assertEqual(resp.code, 200)
  479. # These methods don't allow a body.
  480. for method in ("GET", "DELETE", "OPTIONS"):
  481. with self.assertRaises(ValueError) as context:
  482. self.fetch(
  483. "/all_methods", method=method, body=b"asdf", raise_error=True
  484. )
  485. self.assertIn("must be None", str(context.exception))
  486. # In most cases this can be overridden, but curl_httpclient
  487. # does not allow body with a GET at all.
  488. if method != "GET":
  489. self.fetch(
  490. "/all_methods",
  491. method=method,
  492. body=b"asdf",
  493. allow_nonstandard_methods=True,
  494. raise_error=True,
  495. )
  496. self.assertEqual(resp.code, 200)
  497. # This test causes odd failures with the combination of
  498. # curl_httpclient (at least with the version of libcurl available
  499. # on ubuntu 12.04), TwistedIOLoop, and epoll. For POST (but not PUT),
  500. # curl decides the response came back too soon and closes the connection
  501. # to start again. It does this *before* telling the socket callback to
  502. # unregister the FD. Some IOLoop implementations have special kernel
  503. # integration to discover this immediately. Tornado's IOLoops
  504. # ignore errors on remove_handler to accommodate this behavior, but
  505. # Twisted's reactor does not. The removeReader call fails and so
  506. # do all future removeAll calls (which our tests do at cleanup).
  507. #
  508. # def test_post_307(self):
  509. # response = self.fetch("/redirect?status=307&url=/post",
  510. # method="POST", body=b"arg1=foo&arg2=bar")
  511. # self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
  512. def test_put_307(self):
  513. response = self.fetch(
  514. "/redirect?status=307&url=/put", method="PUT", body=b"hello"
  515. )
  516. response.rethrow()
  517. self.assertEqual(response.body, b"Put body: hello")
  518. def test_non_ascii_header(self):
  519. # Non-ascii headers are sent as latin1.
  520. response = self.fetch("/set_header?k=foo&v=%E9")
  521. response.rethrow()
  522. self.assertEqual(response.headers["Foo"], native_str(u"\u00e9"))
  523. def test_response_times(self):
  524. # A few simple sanity checks of the response time fields to
  525. # make sure they're using the right basis (between the
  526. # wall-time and monotonic clocks).
  527. start_time = time.time()
  528. response = self.fetch("/hello")
  529. response.rethrow()
  530. self.assertGreaterEqual(response.request_time, 0)
  531. self.assertLess(response.request_time, 1.0)
  532. # A very crude check to make sure that start_time is based on
  533. # wall time and not the monotonic clock.
  534. self.assertLess(abs(response.start_time - start_time), 1.0)
  535. for k, v in response.time_info.items():
  536. self.assertTrue(0 <= v < 1.0, "time_info[%s] out of bounds: %s" % (k, v))
  537. @gen_test
  538. def test_error_after_cancel(self):
  539. fut = self.http_client.fetch(self.get_url("/404"))
  540. self.assertTrue(fut.cancel())
  541. with ExpectLog(app_log, "Exception after Future was cancelled") as el:
  542. # We can't wait on the cancelled Future any more, so just
  543. # let the IOLoop run until the exception gets logged (or
  544. # not, in which case we exit the loop and ExpectLog will
  545. # raise).
  546. for i in range(100):
  547. yield gen.sleep(0.01)
  548. if el.logged_stack:
  549. break
  550. class RequestProxyTest(unittest.TestCase):
  551. def test_request_set(self):
  552. proxy = _RequestProxy(
  553. HTTPRequest("http://example.com/", user_agent="foo"), dict()
  554. )
  555. self.assertEqual(proxy.user_agent, "foo")
  556. def test_default_set(self):
  557. proxy = _RequestProxy(
  558. HTTPRequest("http://example.com/"), dict(network_interface="foo")
  559. )
  560. self.assertEqual(proxy.network_interface, "foo")
  561. def test_both_set(self):
  562. proxy = _RequestProxy(
  563. HTTPRequest("http://example.com/", proxy_host="foo"), dict(proxy_host="bar")
  564. )
  565. self.assertEqual(proxy.proxy_host, "foo")
  566. def test_neither_set(self):
  567. proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
  568. self.assertIs(proxy.auth_username, None)
  569. def test_bad_attribute(self):
  570. proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
  571. with self.assertRaises(AttributeError):
  572. proxy.foo
  573. def test_defaults_none(self):
  574. proxy = _RequestProxy(HTTPRequest("http://example.com/"), None)
  575. self.assertIs(proxy.auth_username, None)
  576. class HTTPResponseTestCase(unittest.TestCase):
  577. def test_str(self):
  578. response = HTTPResponse( # type: ignore
  579. HTTPRequest("http://example.com"), 200, headers={}, buffer=BytesIO()
  580. )
  581. s = str(response)
  582. self.assertTrue(s.startswith("HTTPResponse("))
  583. self.assertIn("code=200", s)
  584. class SyncHTTPClientTest(unittest.TestCase):
  585. def setUp(self):
  586. self.server_ioloop = IOLoop()
  587. event = threading.Event()
  588. @gen.coroutine
  589. def init_server():
  590. sock, self.port = bind_unused_port()
  591. app = Application([("/", HelloWorldHandler)])
  592. self.server = HTTPServer(app)
  593. self.server.add_socket(sock)
  594. event.set()
  595. def start():
  596. self.server_ioloop.run_sync(init_server)
  597. self.server_ioloop.start()
  598. self.server_thread = threading.Thread(target=start)
  599. self.server_thread.start()
  600. event.wait()
  601. self.http_client = HTTPClient()
  602. def tearDown(self):
  603. def stop_server():
  604. self.server.stop()
  605. # Delay the shutdown of the IOLoop by several iterations because
  606. # the server may still have some cleanup work left when
  607. # the client finishes with the response (this is noticeable
  608. # with http/2, which leaves a Future with an unexamined
  609. # StreamClosedError on the loop).
  610. @gen.coroutine
  611. def slow_stop():
  612. yield self.server.close_all_connections()
  613. # The number of iterations is difficult to predict. Typically,
  614. # one is sufficient, although sometimes it needs more.
  615. for i in range(5):
  616. yield
  617. self.server_ioloop.stop()
  618. self.server_ioloop.add_callback(slow_stop)
  619. self.server_ioloop.add_callback(stop_server)
  620. self.server_thread.join()
  621. self.http_client.close()
  622. self.server_ioloop.close(all_fds=True)
  623. def get_url(self, path):
  624. return "http://127.0.0.1:%d%s" % (self.port, path)
  625. def test_sync_client(self):
  626. response = self.http_client.fetch(self.get_url("/"))
  627. self.assertEqual(b"Hello world!", response.body)
  628. def test_sync_client_error(self):
  629. # Synchronous HTTPClient raises errors directly; no need for
  630. # response.rethrow()
  631. with self.assertRaises(HTTPError) as assertion:
  632. self.http_client.fetch(self.get_url("/notfound"))
  633. self.assertEqual(assertion.exception.code, 404)
  634. class SyncHTTPClientSubprocessTest(unittest.TestCase):
  635. def test_destructor_log(self):
  636. # Regression test for
  637. # https://github.com/tornadoweb/tornado/issues/2539
  638. #
  639. # In the past, the following program would log an
  640. # "inconsistent AsyncHTTPClient cache" error from a destructor
  641. # when the process is shutting down. The shutdown process is
  642. # subtle and I don't fully understand it; the failure does not
  643. # manifest if that lambda isn't there or is a simpler object
  644. # like an int (nor does it manifest in the tornado test suite
  645. # as a whole, which is why we use this subprocess).
  646. proc = subprocess.run(
  647. [
  648. sys.executable,
  649. "-c",
  650. "from tornado.httpclient import HTTPClient; f = lambda: None; c = HTTPClient()",
  651. ],
  652. stdout=subprocess.PIPE,
  653. stderr=subprocess.STDOUT,
  654. check=True,
  655. )
  656. if proc.stdout:
  657. print("STDOUT:")
  658. print(to_unicode(proc.stdout))
  659. if proc.stdout:
  660. self.fail("subprocess produced unexpected output")
  661. class HTTPRequestTestCase(unittest.TestCase):
  662. def test_headers(self):
  663. request = HTTPRequest("http://example.com", headers={"foo": "bar"})
  664. self.assertEqual(request.headers, {"foo": "bar"})
  665. def test_headers_setter(self):
  666. request = HTTPRequest("http://example.com")
  667. request.headers = {"bar": "baz"} # type: ignore
  668. self.assertEqual(request.headers, {"bar": "baz"})
  669. def test_null_headers_setter(self):
  670. request = HTTPRequest("http://example.com")
  671. request.headers = None # type: ignore
  672. self.assertEqual(request.headers, {})
  673. def test_body(self):
  674. request = HTTPRequest("http://example.com", body="foo")
  675. self.assertEqual(request.body, utf8("foo"))
  676. def test_body_setter(self):
  677. request = HTTPRequest("http://example.com")
  678. request.body = "foo" # type: ignore
  679. self.assertEqual(request.body, utf8("foo"))
  680. def test_if_modified_since(self):
  681. http_date = datetime.datetime.utcnow()
  682. request = HTTPRequest("http://example.com", if_modified_since=http_date)
  683. self.assertEqual(
  684. request.headers, {"If-Modified-Since": format_timestamp(http_date)}
  685. )
  686. class HTTPErrorTestCase(unittest.TestCase):
  687. def test_copy(self):
  688. e = HTTPError(403)
  689. e2 = copy.copy(e)
  690. self.assertIsNot(e, e2)
  691. self.assertEqual(e.code, e2.code)
  692. def test_plain_error(self):
  693. e = HTTPError(403)
  694. self.assertEqual(str(e), "HTTP 403: Forbidden")
  695. self.assertEqual(repr(e), "HTTP 403: Forbidden")
  696. def test_error_with_response(self):
  697. resp = HTTPResponse(HTTPRequest("http://example.com/"), 403)
  698. with self.assertRaises(HTTPError) as cm:
  699. resp.rethrow()
  700. e = cm.exception
  701. self.assertEqual(str(e), "HTTP 403: Forbidden")
  702. self.assertEqual(repr(e), "HTTP 403: Forbidden")