httpclient_test.py 28 KB

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