httpclient_test.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957
  1. import base64
  2. import binascii
  3. from contextlib import closing
  4. import copy
  5. import gzip
  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 ignore_deprecation
  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_representation_headers(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. assert self.request.method is not None
  99. self.write(self.request.method)
  100. get = head = post = put = delete = options = patch = other = method # type: ignore
  101. class SetHeaderHandler(RequestHandler):
  102. def get(self):
  103. # Use get_arguments for keys to get strings, but
  104. # request.arguments for values to get bytes.
  105. for k, v in zip(self.get_arguments("k"), self.request.arguments["v"]):
  106. self.set_header(k, v)
  107. class InvalidGzipHandler(RequestHandler):
  108. def get(self) -> None:
  109. # set Content-Encoding manually to avoid automatic gzip encoding
  110. self.set_header("Content-Type", "text/plain")
  111. self.set_header("Content-Encoding", "gzip")
  112. # Triggering the potential bug seems to depend on input length.
  113. # This length is taken from the bad-response example reported in
  114. # https://github.com/tornadoweb/tornado/pull/2875 (uncompressed).
  115. text = "".join(f"Hello World {i}\n" for i in range(9000))[:149051]
  116. body = gzip.compress(text.encode(), compresslevel=6) + b"\00"
  117. self.write(body)
  118. class HeaderEncodingHandler(RequestHandler):
  119. def get(self):
  120. self.finish(self.request.headers["Foo"].encode("ISO8859-1"))
  121. # These tests end up getting run redundantly: once here with the default
  122. # HTTPClient implementation, and then again in each implementation's own
  123. # test suite.
  124. class HTTPClientCommonTestCase(AsyncHTTPTestCase):
  125. def get_app(self):
  126. return Application(
  127. [
  128. url("/hello", HelloWorldHandler),
  129. url("/post", PostHandler),
  130. url("/put", PutHandler),
  131. url("/redirect", RedirectHandler),
  132. url("/redirect_without_location", RedirectWithoutLocationHandler),
  133. url("/chunk", ChunkHandler),
  134. url("/auth", AuthHandler),
  135. url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
  136. url("/echopost", EchoPostHandler),
  137. url("/user_agent", UserAgentHandler),
  138. url("/304_with_content_length", ContentLength304Handler),
  139. url("/all_methods", AllMethodsHandler),
  140. url("/patch", PatchHandler),
  141. url("/set_header", SetHeaderHandler),
  142. url("/invalid_gzip", InvalidGzipHandler),
  143. url("/header-encoding", HeaderEncodingHandler),
  144. ],
  145. gzip=True,
  146. )
  147. def test_patch_receives_payload(self):
  148. body = b"some patch data"
  149. response = self.fetch("/patch", method="PATCH", body=body)
  150. self.assertEqual(response.code, 200)
  151. self.assertEqual(response.body, body)
  152. def test_hello_world(self):
  153. response = self.fetch("/hello")
  154. self.assertEqual(response.code, 200)
  155. self.assertEqual(response.headers["Content-Type"], "text/plain")
  156. self.assertEqual(response.body, b"Hello world!")
  157. assert response.request_time is not None
  158. self.assertEqual(int(response.request_time), 0)
  159. response = self.fetch("/hello?name=Ben")
  160. self.assertEqual(response.body, b"Hello Ben!")
  161. def test_streaming_callback(self):
  162. # streaming_callback is also tested in test_chunked
  163. chunks = [] # type: typing.List[bytes]
  164. response = self.fetch("/hello", streaming_callback=chunks.append)
  165. # with streaming_callback, data goes to the callback and not response.body
  166. self.assertEqual(chunks, [b"Hello world!"])
  167. self.assertFalse(response.body)
  168. def test_post(self):
  169. response = self.fetch("/post", method="POST", body="arg1=foo&arg2=bar")
  170. self.assertEqual(response.code, 200)
  171. self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
  172. def test_chunked(self):
  173. response = self.fetch("/chunk")
  174. self.assertEqual(response.body, b"asdfqwer")
  175. chunks = [] # type: typing.List[bytes]
  176. response = self.fetch("/chunk", streaming_callback=chunks.append)
  177. self.assertEqual(chunks, [b"asdf", b"qwer"])
  178. self.assertFalse(response.body)
  179. def test_chunked_close(self):
  180. # test case in which chunks spread read-callback processing
  181. # over several ioloop iterations, but the connection is already closed.
  182. sock, port = bind_unused_port()
  183. with closing(sock):
  184. @gen.coroutine
  185. def accept_callback(conn, address):
  186. # fake an HTTP server using chunked encoding where the final chunks
  187. # and connection close all happen at once
  188. stream = IOStream(conn)
  189. request_data = yield stream.read_until(b"\r\n\r\n")
  190. if b"HTTP/1." not in request_data:
  191. self.skipTest("requires HTTP/1.x")
  192. yield stream.write(
  193. b"""\
  194. HTTP/1.1 200 OK
  195. Transfer-Encoding: chunked
  196. 1
  197. 1
  198. 1
  199. 2
  200. 0
  201. """.replace(
  202. b"\n", b"\r\n"
  203. )
  204. )
  205. stream.close()
  206. netutil.add_accept_handler(sock, accept_callback) # type: ignore
  207. resp = self.fetch("http://127.0.0.1:%d/" % port)
  208. resp.rethrow()
  209. self.assertEqual(resp.body, b"12")
  210. self.io_loop.remove_handler(sock.fileno())
  211. def test_basic_auth(self):
  212. # This test data appears in section 2 of RFC 7617.
  213. self.assertEqual(
  214. self.fetch(
  215. "/auth", auth_username="Aladdin", auth_password="open sesame"
  216. ).body,
  217. b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
  218. )
  219. def test_basic_auth_explicit_mode(self):
  220. self.assertEqual(
  221. self.fetch(
  222. "/auth",
  223. auth_username="Aladdin",
  224. auth_password="open sesame",
  225. auth_mode="basic",
  226. ).body,
  227. b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
  228. )
  229. def test_basic_auth_unicode(self):
  230. # This test data appears in section 2.1 of RFC 7617.
  231. self.assertEqual(
  232. self.fetch("/auth", auth_username="test", auth_password="123£").body,
  233. b"Basic dGVzdDoxMjPCow==",
  234. )
  235. # The standard mandates NFC. Give it a decomposed username
  236. # and ensure it is normalized to composed form.
  237. username = unicodedata.normalize("NFD", "josé")
  238. self.assertEqual(
  239. self.fetch("/auth", auth_username=username, auth_password="səcrət").body,
  240. b"Basic am9zw6k6c8mZY3LJmXQ=",
  241. )
  242. def test_unsupported_auth_mode(self):
  243. # curl and simple clients handle errors a bit differently; the
  244. # important thing is that they don't fall back to basic auth
  245. # on an unknown mode.
  246. with ExpectLog(gen_log, "uncaught exception", required=False):
  247. with self.assertRaises((ValueError, HTTPError)): # type: ignore
  248. self.fetch(
  249. "/auth",
  250. auth_username="Aladdin",
  251. auth_password="open sesame",
  252. auth_mode="asdf",
  253. raise_error=True,
  254. )
  255. def test_follow_redirect(self):
  256. response = self.fetch("/countdown/2", follow_redirects=False)
  257. self.assertEqual(302, response.code)
  258. self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
  259. response = self.fetch("/countdown/2")
  260. self.assertEqual(200, response.code)
  261. self.assertTrue(response.effective_url.endswith("/countdown/0"))
  262. self.assertEqual(b"Zero", response.body)
  263. def test_redirect_without_location(self):
  264. response = self.fetch("/redirect_without_location", follow_redirects=True)
  265. # If there is no location header, the redirect response should
  266. # just be returned as-is. (This should arguably raise an
  267. # error, but libcurl doesn't treat this as an error, so we
  268. # don't either).
  269. self.assertEqual(301, response.code)
  270. def test_redirect_put_with_body(self):
  271. response = self.fetch(
  272. "/redirect?url=/put&status=307", method="PUT", body="hello"
  273. )
  274. self.assertEqual(response.body, b"Put body: hello")
  275. def test_redirect_put_without_body(self):
  276. # This "without body" edge case is similar to what happens with body_producer.
  277. response = self.fetch(
  278. "/redirect?url=/put&status=307",
  279. method="PUT",
  280. allow_nonstandard_methods=True,
  281. )
  282. self.assertEqual(response.body, b"Put body: ")
  283. def test_method_after_redirect(self):
  284. # Legacy redirect codes (301, 302) convert POST requests to GET.
  285. for status in [301, 302, 303]:
  286. url = "/redirect?url=/all_methods&status=%d" % status
  287. resp = self.fetch(url, method="POST", body=b"")
  288. self.assertEqual(b"GET", resp.body)
  289. # Other methods are left alone, except for 303 redirect, depending on client
  290. for method in ["GET", "OPTIONS", "PUT", "DELETE"]:
  291. resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
  292. if status in [301, 302]:
  293. self.assertEqual(utf8(method), resp.body)
  294. else:
  295. self.assertIn(resp.body, [utf8(method), b"GET"])
  296. # HEAD is different so check it separately.
  297. resp = self.fetch(url, method="HEAD")
  298. self.assertEqual(200, resp.code)
  299. self.assertEqual(b"", resp.body)
  300. # Newer redirects always preserve the original method.
  301. for status in [307, 308]:
  302. url = "/redirect?url=/all_methods&status=307"
  303. for method in ["GET", "OPTIONS", "POST", "PUT", "DELETE"]:
  304. resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
  305. self.assertEqual(method, to_unicode(resp.body))
  306. resp = self.fetch(url, method="HEAD")
  307. self.assertEqual(200, resp.code)
  308. self.assertEqual(b"", resp.body)
  309. def test_credentials_in_url(self):
  310. url = self.get_url("/auth").replace("http://", "http://me:secret@")
  311. response = self.fetch(url)
  312. self.assertEqual(b"Basic " + base64.b64encode(b"me:secret"), response.body)
  313. def test_body_encoding(self):
  314. unicode_body = "\xe9"
  315. byte_body = binascii.a2b_hex(b"e9")
  316. # unicode string in body gets converted to utf8
  317. response = self.fetch(
  318. "/echopost",
  319. method="POST",
  320. body=unicode_body,
  321. headers={"Content-Type": "application/blah"},
  322. )
  323. self.assertEqual(response.headers["Content-Length"], "2")
  324. self.assertEqual(response.body, utf8(unicode_body))
  325. # byte strings pass through directly
  326. response = self.fetch(
  327. "/echopost",
  328. method="POST",
  329. body=byte_body,
  330. headers={"Content-Type": "application/blah"},
  331. )
  332. self.assertEqual(response.headers["Content-Length"], "1")
  333. self.assertEqual(response.body, byte_body)
  334. # Mixing unicode in headers and byte string bodies shouldn't
  335. # break anything
  336. response = self.fetch(
  337. "/echopost",
  338. method="POST",
  339. body=byte_body,
  340. headers={"Content-Type": "application/blah"},
  341. user_agent="foo",
  342. )
  343. self.assertEqual(response.headers["Content-Length"], "1")
  344. self.assertEqual(response.body, byte_body)
  345. def test_types(self):
  346. response = self.fetch("/hello")
  347. self.assertEqual(type(response.body), bytes)
  348. self.assertEqual(type(response.headers["Content-Type"]), str)
  349. self.assertEqual(type(response.code), int)
  350. self.assertEqual(type(response.effective_url), str)
  351. def test_gzip(self):
  352. # All the tests in this file should be using gzip, but this test
  353. # ensures that it is in fact getting compressed, and also tests
  354. # the httpclient's decompress=False option.
  355. # Setting Accept-Encoding manually bypasses the client's
  356. # decompression so we can see the raw data.
  357. response = self.fetch(
  358. "/chunk", decompress_response=False, headers={"Accept-Encoding": "gzip"}
  359. )
  360. self.assertEqual(response.headers["Content-Encoding"], "gzip")
  361. self.assertNotEqual(response.body, b"asdfqwer")
  362. # Our test data gets bigger when gzipped. Oops. :)
  363. # Chunked encoding bypasses the MIN_LENGTH check.
  364. self.assertEqual(len(response.body), 34)
  365. f = gzip.GzipFile(mode="r", fileobj=response.buffer)
  366. self.assertEqual(f.read(), b"asdfqwer")
  367. def test_invalid_gzip(self):
  368. # test if client hangs on tricky invalid gzip
  369. # curl/simple httpclient have different behavior (exception, logging)
  370. with ExpectLog(
  371. gen_log, ".*Malformed HTTP message.*unconsumed gzip data", required=False
  372. ):
  373. try:
  374. response = self.fetch("/invalid_gzip")
  375. self.assertEqual(response.code, 200)
  376. self.assertEqual(response.body[:14], b"Hello World 0\n")
  377. except HTTPError:
  378. pass # acceptable
  379. def test_header_callback(self):
  380. first_line = []
  381. headers = {}
  382. chunks = []
  383. def header_callback(header_line):
  384. if header_line.startswith("HTTP/1.1 101"):
  385. # Upgrading to HTTP/2
  386. pass
  387. elif header_line.startswith("HTTP/"):
  388. first_line.append(header_line)
  389. elif header_line != "\r\n":
  390. k, v = header_line.split(":", 1)
  391. headers[k.lower()] = v.strip()
  392. def streaming_callback(chunk):
  393. # All header callbacks are run before any streaming callbacks,
  394. # so the header data is available to process the data as it
  395. # comes in.
  396. self.assertEqual(headers["content-type"], "text/html; charset=UTF-8")
  397. chunks.append(chunk)
  398. self.fetch(
  399. "/chunk",
  400. header_callback=header_callback,
  401. streaming_callback=streaming_callback,
  402. )
  403. self.assertEqual(len(first_line), 1, first_line)
  404. self.assertRegex(first_line[0], "HTTP/[0-9]\\.[0-9] 200.*\r\n")
  405. self.assertEqual(chunks, [b"asdf", b"qwer"])
  406. def test_header_callback_to_parse_line(self):
  407. # Make a request with header_callback and feed the headers to HTTPHeaders.parse_line.
  408. # (Instead of HTTPHeaders.parse which is used in normal cases). Ensure that the resulting
  409. # headers are as expected, and in particular do not have trailing whitespace added
  410. # due to the final CRLF line.
  411. headers = HTTPHeaders()
  412. def header_callback(line):
  413. if line.startswith("HTTP/"):
  414. # Ignore the first status line
  415. return
  416. headers.parse_line(line)
  417. self.fetch("/hello", header_callback=header_callback)
  418. for k, v in headers.get_all():
  419. self.assertTrue(v == v.strip(), (k, v))
  420. @gen_test
  421. def test_configure_defaults(self):
  422. defaults = dict(user_agent="TestDefaultUserAgent", allow_ipv6=False)
  423. # Construct a new instance of the configured client class
  424. client = self.http_client.__class__(force_instance=True, defaults=defaults)
  425. try:
  426. response = yield client.fetch(self.get_url("/user_agent"))
  427. self.assertEqual(response.body, b"TestDefaultUserAgent")
  428. finally:
  429. client.close()
  430. def test_header_types(self):
  431. # Header values may be passed as character or utf8 byte strings,
  432. # in a plain dictionary or an HTTPHeaders object.
  433. # Keys must always be the native str type.
  434. # All combinations should have the same results on the wire.
  435. for value in ["MyUserAgent", b"MyUserAgent"]:
  436. for container in [dict, HTTPHeaders]:
  437. headers = container()
  438. headers["User-Agent"] = value
  439. resp = self.fetch("/user_agent", headers=headers)
  440. self.assertEqual(
  441. resp.body,
  442. b"MyUserAgent",
  443. "response=%r, value=%r, container=%r"
  444. % (resp.body, value, container),
  445. )
  446. def test_multi_line_headers(self):
  447. # Multi-line http headers are rare but rfc-allowed
  448. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
  449. sock, port = bind_unused_port()
  450. with closing(sock):
  451. @gen.coroutine
  452. def accept_callback(conn, address):
  453. stream = IOStream(conn)
  454. request_data = yield stream.read_until(b"\r\n\r\n")
  455. if b"HTTP/1." not in request_data:
  456. self.skipTest("requires HTTP/1.x")
  457. yield stream.write(
  458. b"""\
  459. HTTP/1.1 200 OK
  460. X-XSS-Protection: 1;
  461. \tmode=block
  462. """.replace(
  463. b"\n", b"\r\n"
  464. )
  465. )
  466. stream.close()
  467. netutil.add_accept_handler(sock, accept_callback) # type: ignore
  468. try:
  469. resp = self.fetch("http://127.0.0.1:%d/" % port)
  470. resp.rethrow()
  471. self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
  472. finally:
  473. self.io_loop.remove_handler(sock.fileno())
  474. @gen_test
  475. def test_header_encoding(self):
  476. response = yield self.http_client.fetch(
  477. self.get_url("/header-encoding"),
  478. headers={
  479. "Foo": "b\xe4r",
  480. },
  481. )
  482. self.assertEqual(response.body, "b\xe4r".encode("ISO8859-1"))
  483. def test_304_with_content_length(self):
  484. # According to the spec 304 responses SHOULD NOT include
  485. # Content-Length or other entity headers, but some servers do it
  486. # anyway.
  487. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
  488. response = self.fetch("/304_with_content_length")
  489. self.assertEqual(response.code, 304)
  490. self.assertEqual(response.headers["Content-Length"], "42")
  491. @gen_test
  492. def test_future_interface(self):
  493. response = yield self.http_client.fetch(self.get_url("/hello"))
  494. self.assertEqual(response.body, b"Hello world!")
  495. @gen_test
  496. def test_future_http_error(self):
  497. with self.assertRaises(HTTPError) as context:
  498. yield self.http_client.fetch(self.get_url("/notfound"))
  499. assert context.exception is not None
  500. assert context.exception.response is not None
  501. self.assertEqual(context.exception.code, 404)
  502. self.assertEqual(context.exception.response.code, 404)
  503. @gen_test
  504. def test_future_http_error_no_raise(self):
  505. response = yield self.http_client.fetch(
  506. self.get_url("/notfound"), raise_error=False
  507. )
  508. self.assertEqual(response.code, 404)
  509. @gen_test
  510. def test_reuse_request_from_response(self):
  511. # The response.request attribute should be an HTTPRequest, not
  512. # a _RequestProxy.
  513. # This test uses self.http_client.fetch because self.fetch calls
  514. # self.get_url on the input unconditionally.
  515. url = self.get_url("/hello")
  516. response = yield self.http_client.fetch(url)
  517. self.assertEqual(response.request.url, url)
  518. self.assertTrue(isinstance(response.request, HTTPRequest))
  519. response2 = yield self.http_client.fetch(response.request)
  520. self.assertEqual(response2.body, b"Hello world!")
  521. @gen_test
  522. def test_bind_source_ip(self):
  523. url = self.get_url("/hello")
  524. request = HTTPRequest(url, network_interface="127.0.0.1")
  525. response = yield self.http_client.fetch(request)
  526. self.assertEqual(response.code, 200)
  527. with self.assertRaises((ValueError, HTTPError)) as context: # type: ignore
  528. request = HTTPRequest(url, network_interface="not-interface-or-ip")
  529. yield self.http_client.fetch(request)
  530. self.assertIn("not-interface-or-ip", str(context.exception))
  531. def test_all_methods(self):
  532. for method in ["GET", "DELETE", "OPTIONS"]:
  533. response = self.fetch("/all_methods", method=method)
  534. self.assertEqual(response.body, utf8(method))
  535. for method in ["POST", "PUT", "PATCH"]:
  536. response = self.fetch("/all_methods", method=method, body=b"")
  537. self.assertEqual(response.body, utf8(method))
  538. response = self.fetch("/all_methods", method="HEAD")
  539. self.assertEqual(response.body, b"")
  540. response = self.fetch(
  541. "/all_methods", method="OTHER", allow_nonstandard_methods=True
  542. )
  543. self.assertEqual(response.body, b"OTHER")
  544. def test_body_sanity_checks(self):
  545. # These methods require a body.
  546. for method in ("POST", "PUT", "PATCH"):
  547. with self.assertRaises(ValueError) as context:
  548. self.fetch("/all_methods", method=method, raise_error=True)
  549. self.assertIn("must not be None", str(context.exception))
  550. resp = self.fetch(
  551. "/all_methods", method=method, allow_nonstandard_methods=True
  552. )
  553. self.assertEqual(resp.code, 200)
  554. # These methods don't allow a body.
  555. for method in ("GET", "DELETE", "OPTIONS"):
  556. with self.assertRaises(ValueError) as context:
  557. self.fetch(
  558. "/all_methods", method=method, body=b"asdf", raise_error=True
  559. )
  560. self.assertIn("must be None", str(context.exception))
  561. # In most cases this can be overridden, but curl_httpclient
  562. # does not allow body with a GET at all.
  563. if method != "GET":
  564. self.fetch(
  565. "/all_methods",
  566. method=method,
  567. body=b"asdf",
  568. allow_nonstandard_methods=True,
  569. raise_error=True,
  570. )
  571. self.assertEqual(resp.code, 200)
  572. # This test causes odd failures with the combination of
  573. # curl_httpclient (at least with the version of libcurl available
  574. # on ubuntu 12.04), TwistedIOLoop, and epoll. For POST (but not PUT),
  575. # curl decides the response came back too soon and closes the connection
  576. # to start again. It does this *before* telling the socket callback to
  577. # unregister the FD. Some IOLoop implementations have special kernel
  578. # integration to discover this immediately. Tornado's IOLoops
  579. # ignore errors on remove_handler to accommodate this behavior, but
  580. # Twisted's reactor does not. The removeReader call fails and so
  581. # do all future removeAll calls (which our tests do at cleanup).
  582. #
  583. # def test_post_307(self):
  584. # response = self.fetch("/redirect?status=307&url=/post",
  585. # method="POST", body=b"arg1=foo&arg2=bar")
  586. # self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
  587. def test_put_307(self):
  588. response = self.fetch(
  589. "/redirect?status=307&url=/put", method="PUT", body=b"hello"
  590. )
  591. response.rethrow()
  592. self.assertEqual(response.body, b"Put body: hello")
  593. def test_non_ascii_header(self):
  594. # Non-ascii headers are sent as latin1.
  595. response = self.fetch("/set_header?k=foo&v=%E9")
  596. response.rethrow()
  597. self.assertEqual(response.headers["Foo"], native_str("\u00e9"))
  598. def test_response_times(self):
  599. # A few simple sanity checks of the response time fields to
  600. # make sure they're using the right basis (between the
  601. # wall-time and monotonic clocks).
  602. start_time = time.time()
  603. response = self.fetch("/hello")
  604. response.rethrow()
  605. self.assertIsNotNone(response.request_time)
  606. assert response.request_time is not None # for mypy
  607. self.assertGreaterEqual(response.request_time, 0)
  608. self.assertLess(response.request_time, 1.0)
  609. # A very crude check to make sure that start_time is based on
  610. # wall time and not the monotonic clock.
  611. self.assertIsNotNone(response.start_time)
  612. assert response.start_time is not None # for mypy
  613. self.assertLess(abs(response.start_time - start_time), 1.0)
  614. for k, v in response.time_info.items():
  615. self.assertTrue(0 <= v < 1.0, f"time_info[{k}] out of bounds: {v}")
  616. def test_zero_timeout(self):
  617. response = self.fetch("/hello", connect_timeout=0)
  618. self.assertEqual(response.code, 200)
  619. response = self.fetch("/hello", request_timeout=0)
  620. self.assertEqual(response.code, 200)
  621. response = self.fetch("/hello", connect_timeout=0, request_timeout=0)
  622. self.assertEqual(response.code, 200)
  623. @gen_test
  624. def test_error_after_cancel(self):
  625. fut = self.http_client.fetch(self.get_url("/404"))
  626. self.assertTrue(fut.cancel())
  627. with ExpectLog(app_log, "Exception after Future was cancelled") as el:
  628. # We can't wait on the cancelled Future any more, so just
  629. # let the IOLoop run until the exception gets logged (or
  630. # not, in which case we exit the loop and ExpectLog will
  631. # raise).
  632. for i in range(100):
  633. yield gen.sleep(0.01)
  634. if el.logged_stack:
  635. break
  636. def test_header_crlf(self):
  637. # Ensure that the client doesn't allow CRLF injection in headers. RFC 9112 section 2.2
  638. # prohibits a bare CR specifically and "a recipient MAY recognize a single LF as a line
  639. # terminator" so we check each character separately as well as the (redundant) CRLF pair.
  640. for header, name in [
  641. ("foo\rbar:", "cr"),
  642. ("foo\nbar:", "lf"),
  643. ("foo\r\nbar:", "crlf"),
  644. ]:
  645. with self.subTest(name=name, position="value"):
  646. with self.assertRaises(ValueError):
  647. self.fetch("/hello", headers={"foo": header})
  648. with self.subTest(name=name, position="key"):
  649. with self.assertRaises(ValueError):
  650. self.fetch("/hello", headers={header: "foo"})
  651. class RequestProxyTest(unittest.TestCase):
  652. def test_request_set(self):
  653. proxy = _RequestProxy(
  654. HTTPRequest("http://example.com/", user_agent="foo"), dict()
  655. )
  656. self.assertEqual(proxy.user_agent, "foo")
  657. def test_default_set(self):
  658. proxy = _RequestProxy(
  659. HTTPRequest("http://example.com/"), dict(network_interface="foo")
  660. )
  661. self.assertEqual(proxy.network_interface, "foo")
  662. def test_both_set(self):
  663. proxy = _RequestProxy(
  664. HTTPRequest("http://example.com/", proxy_host="foo"), dict(proxy_host="bar")
  665. )
  666. self.assertEqual(proxy.proxy_host, "foo")
  667. def test_neither_set(self):
  668. proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
  669. self.assertIsNone(proxy.auth_username)
  670. def test_bad_attribute(self):
  671. proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
  672. with self.assertRaises(AttributeError):
  673. proxy.foo
  674. def test_defaults_none(self):
  675. proxy = _RequestProxy(HTTPRequest("http://example.com/"), None)
  676. self.assertIsNone(proxy.auth_username)
  677. class HTTPResponseTestCase(unittest.TestCase):
  678. def test_str(self):
  679. response = HTTPResponse( # type: ignore
  680. HTTPRequest("http://example.com"), 200, buffer=BytesIO()
  681. )
  682. s = str(response)
  683. self.assertTrue(s.startswith("HTTPResponse("))
  684. self.assertIn("code=200", s)
  685. class SyncHTTPClientTest(unittest.TestCase):
  686. def setUp(self):
  687. self.server_ioloop = IOLoop(make_current=False)
  688. event = threading.Event()
  689. @gen.coroutine
  690. def init_server():
  691. sock, self.port = bind_unused_port()
  692. app = Application([("/", HelloWorldHandler)])
  693. self.server = HTTPServer(app)
  694. self.server.add_socket(sock)
  695. event.set()
  696. def start():
  697. self.server_ioloop.run_sync(init_server)
  698. self.server_ioloop.start()
  699. self.server_thread = threading.Thread(target=start)
  700. self.server_thread.start()
  701. event.wait()
  702. self.http_client = HTTPClient()
  703. def tearDown(self):
  704. def stop_server():
  705. self.server.stop()
  706. # Delay the shutdown of the IOLoop by several iterations because
  707. # the server may still have some cleanup work left when
  708. # the client finishes with the response (this is noticeable
  709. # with http/2, which leaves a Future with an unexamined
  710. # StreamClosedError on the loop).
  711. @gen.coroutine
  712. def slow_stop():
  713. yield self.server.close_all_connections()
  714. # The number of iterations is difficult to predict. Typically,
  715. # one is sufficient, although sometimes it needs more.
  716. for i in range(5):
  717. yield
  718. self.server_ioloop.stop()
  719. self.server_ioloop.add_callback(slow_stop)
  720. self.server_ioloop.add_callback(stop_server)
  721. self.server_thread.join()
  722. self.http_client.close()
  723. self.server_ioloop.close(all_fds=True)
  724. def get_url(self, path):
  725. return "http://127.0.0.1:%d%s" % (self.port, path)
  726. def test_sync_client(self):
  727. response = self.http_client.fetch(self.get_url("/"))
  728. self.assertEqual(b"Hello world!", response.body)
  729. def test_sync_client_error(self):
  730. # Synchronous HTTPClient raises errors directly; no need for
  731. # response.rethrow()
  732. with self.assertRaises(HTTPError) as assertion:
  733. self.http_client.fetch(self.get_url("/notfound"))
  734. self.assertEqual(assertion.exception.code, 404)
  735. class SyncHTTPClientSubprocessTest(unittest.TestCase):
  736. def test_destructor_log(self):
  737. # Regression test for
  738. # https://github.com/tornadoweb/tornado/issues/2539
  739. #
  740. # In the past, the following program would log an
  741. # "inconsistent AsyncHTTPClient cache" error from a destructor
  742. # when the process is shutting down. The shutdown process is
  743. # subtle and I don't fully understand it; the failure does not
  744. # manifest if that lambda isn't there or is a simpler object
  745. # like an int (nor does it manifest in the tornado test suite
  746. # as a whole, which is why we use this subprocess).
  747. proc = subprocess.run(
  748. [
  749. sys.executable,
  750. "-c",
  751. "from tornado.httpclient import HTTPClient; f = lambda: None; c = HTTPClient()",
  752. ],
  753. stdout=subprocess.PIPE,
  754. stderr=subprocess.STDOUT,
  755. check=True,
  756. timeout=15,
  757. )
  758. if proc.stdout:
  759. print("STDOUT:")
  760. print(to_unicode(proc.stdout))
  761. if proc.stdout:
  762. self.fail("subprocess produced unexpected output")
  763. class HTTPRequestTestCase(unittest.TestCase):
  764. def test_headers(self):
  765. request = HTTPRequest("http://example.com", headers={"foo": "bar"})
  766. self.assertEqual(request.headers, {"foo": "bar"})
  767. def test_headers_setter(self):
  768. request = HTTPRequest("http://example.com")
  769. request.headers = {"bar": "baz"} # type: ignore
  770. self.assertEqual(request.headers, {"bar": "baz"})
  771. def test_null_headers_setter(self):
  772. request = HTTPRequest("http://example.com")
  773. request.headers = None # type: ignore
  774. self.assertEqual(request.headers, {})
  775. def test_body(self):
  776. request = HTTPRequest("http://example.com", body="foo")
  777. self.assertEqual(request.body, utf8("foo"))
  778. def test_body_setter(self):
  779. request = HTTPRequest("http://example.com")
  780. request.body = "foo" # type: ignore
  781. self.assertEqual(request.body, utf8("foo"))
  782. def test_if_modified_since(self):
  783. http_date = datetime.datetime.now(datetime.timezone.utc)
  784. request = HTTPRequest("http://example.com", if_modified_since=http_date)
  785. self.assertEqual(
  786. request.headers, {"If-Modified-Since": format_timestamp(http_date)}
  787. )
  788. def test_if_modified_since_naive_deprecated(self):
  789. with ignore_deprecation():
  790. http_date = datetime.datetime.utcnow()
  791. request = HTTPRequest("http://example.com", if_modified_since=http_date)
  792. self.assertEqual(
  793. request.headers, {"If-Modified-Since": format_timestamp(http_date)}
  794. )
  795. class HTTPErrorTestCase(unittest.TestCase):
  796. def test_copy(self):
  797. e = HTTPError(403)
  798. e2 = copy.copy(e)
  799. self.assertIsNot(e, e2)
  800. self.assertEqual(e.code, e2.code)
  801. def test_plain_error(self):
  802. e = HTTPError(403)
  803. self.assertEqual(str(e), "HTTP 403: Forbidden")
  804. self.assertEqual(repr(e), "HTTP 403: Forbidden")
  805. def test_error_with_response(self):
  806. resp = HTTPResponse(HTTPRequest("http://example.com/"), 403)
  807. with self.assertRaises(HTTPError) as cm:
  808. resp.rethrow()
  809. e = cm.exception
  810. self.assertEqual(str(e), "HTTP 403: Forbidden")
  811. self.assertEqual(repr(e), "HTTP 403: Forbidden")