| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- # -*- coding: utf-8 -*-
- import base64
- import binascii
- from contextlib import closing
- import copy
- import threading
- import datetime
- from io import BytesIO
- import subprocess
- import sys
- import time
- import typing # noqa: F401
- import unicodedata
- import unittest
- from tornado.escape import utf8, native_str, to_unicode
- from tornado import gen
- from tornado.httpclient import (
- HTTPRequest,
- HTTPResponse,
- _RequestProxy,
- HTTPError,
- HTTPClient,
- )
- from tornado.httpserver import HTTPServer
- from tornado.ioloop import IOLoop
- from tornado.iostream import IOStream
- from tornado.log import gen_log, app_log
- from tornado import netutil
- from tornado.testing import AsyncHTTPTestCase, bind_unused_port, gen_test, ExpectLog
- from tornado.test.util import skipOnTravis
- from tornado.web import Application, RequestHandler, url
- from tornado.httputil import format_timestamp, HTTPHeaders
- class HelloWorldHandler(RequestHandler):
- def get(self):
- name = self.get_argument("name", "world")
- self.set_header("Content-Type", "text/plain")
- self.finish("Hello %s!" % name)
- class PostHandler(RequestHandler):
- def post(self):
- self.finish(
- "Post arg1: %s, arg2: %s"
- % (self.get_argument("arg1"), self.get_argument("arg2"))
- )
- class PutHandler(RequestHandler):
- def put(self):
- self.write("Put body: ")
- self.write(self.request.body)
- class RedirectHandler(RequestHandler):
- def prepare(self):
- self.write("redirects can have bodies too")
- self.redirect(
- self.get_argument("url"), status=int(self.get_argument("status", "302"))
- )
- class RedirectWithoutLocationHandler(RequestHandler):
- def prepare(self):
- # For testing error handling of a redirect with no location header.
- self.set_status(301)
- self.finish()
- class ChunkHandler(RequestHandler):
- @gen.coroutine
- def get(self):
- self.write("asdf")
- self.flush()
- # Wait a bit to ensure the chunks are sent and received separately.
- yield gen.sleep(0.01)
- self.write("qwer")
- class AuthHandler(RequestHandler):
- def get(self):
- self.finish(self.request.headers["Authorization"])
- class CountdownHandler(RequestHandler):
- def get(self, count):
- count = int(count)
- if count > 0:
- self.redirect(self.reverse_url("countdown", count - 1))
- else:
- self.write("Zero")
- class EchoPostHandler(RequestHandler):
- def post(self):
- self.write(self.request.body)
- class UserAgentHandler(RequestHandler):
- def get(self):
- self.write(self.request.headers.get("User-Agent", "User agent not set"))
- class ContentLength304Handler(RequestHandler):
- def get(self):
- self.set_status(304)
- self.set_header("Content-Length", 42)
- def _clear_headers_for_304(self):
- # Tornado strips content-length from 304 responses, but here we
- # want to simulate servers that include the headers anyway.
- pass
- class PatchHandler(RequestHandler):
- def patch(self):
- "Return the request payload - so we can check it is being kept"
- self.write(self.request.body)
- class AllMethodsHandler(RequestHandler):
- SUPPORTED_METHODS = RequestHandler.SUPPORTED_METHODS + ("OTHER",) # type: ignore
- def method(self):
- self.write(self.request.method)
- get = head = post = put = delete = options = patch = other = method # type: ignore
- class SetHeaderHandler(RequestHandler):
- def get(self):
- # Use get_arguments for keys to get strings, but
- # request.arguments for values to get bytes.
- for k, v in zip(self.get_arguments("k"), self.request.arguments["v"]):
- self.set_header(k, v)
- # These tests end up getting run redundantly: once here with the default
- # HTTPClient implementation, and then again in each implementation's own
- # test suite.
- class HTTPClientCommonTestCase(AsyncHTTPTestCase):
- def get_app(self):
- return Application(
- [
- url("/hello", HelloWorldHandler),
- url("/post", PostHandler),
- url("/put", PutHandler),
- url("/redirect", RedirectHandler),
- url("/redirect_without_location", RedirectWithoutLocationHandler),
- url("/chunk", ChunkHandler),
- url("/auth", AuthHandler),
- url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
- url("/echopost", EchoPostHandler),
- url("/user_agent", UserAgentHandler),
- url("/304_with_content_length", ContentLength304Handler),
- url("/all_methods", AllMethodsHandler),
- url("/patch", PatchHandler),
- url("/set_header", SetHeaderHandler),
- ],
- gzip=True,
- )
- def test_patch_receives_payload(self):
- body = b"some patch data"
- response = self.fetch("/patch", method="PATCH", body=body)
- self.assertEqual(response.code, 200)
- self.assertEqual(response.body, body)
- @skipOnTravis
- def test_hello_world(self):
- response = self.fetch("/hello")
- self.assertEqual(response.code, 200)
- self.assertEqual(response.headers["Content-Type"], "text/plain")
- self.assertEqual(response.body, b"Hello world!")
- self.assertEqual(int(response.request_time), 0)
- response = self.fetch("/hello?name=Ben")
- self.assertEqual(response.body, b"Hello Ben!")
- def test_streaming_callback(self):
- # streaming_callback is also tested in test_chunked
- chunks = [] # type: typing.List[bytes]
- response = self.fetch("/hello", streaming_callback=chunks.append)
- # with streaming_callback, data goes to the callback and not response.body
- self.assertEqual(chunks, [b"Hello world!"])
- self.assertFalse(response.body)
- def test_post(self):
- response = self.fetch("/post", method="POST", body="arg1=foo&arg2=bar")
- self.assertEqual(response.code, 200)
- self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
- def test_chunked(self):
- response = self.fetch("/chunk")
- self.assertEqual(response.body, b"asdfqwer")
- chunks = [] # type: typing.List[bytes]
- response = self.fetch("/chunk", streaming_callback=chunks.append)
- self.assertEqual(chunks, [b"asdf", b"qwer"])
- self.assertFalse(response.body)
- def test_chunked_close(self):
- # test case in which chunks spread read-callback processing
- # over several ioloop iterations, but the connection is already closed.
- sock, port = bind_unused_port()
- with closing(sock):
- @gen.coroutine
- def accept_callback(conn, address):
- # fake an HTTP server using chunked encoding where the final chunks
- # and connection close all happen at once
- stream = IOStream(conn)
- request_data = yield stream.read_until(b"\r\n\r\n")
- if b"HTTP/1." not in request_data:
- self.skipTest("requires HTTP/1.x")
- yield stream.write(
- b"""\
- HTTP/1.1 200 OK
- Transfer-Encoding: chunked
- 1
- 1
- 1
- 2
- 0
- """.replace(
- b"\n", b"\r\n"
- )
- )
- stream.close()
- netutil.add_accept_handler(sock, accept_callback) # type: ignore
- resp = self.fetch("http://127.0.0.1:%d/" % port)
- resp.rethrow()
- self.assertEqual(resp.body, b"12")
- self.io_loop.remove_handler(sock.fileno())
- def test_basic_auth(self):
- # This test data appears in section 2 of RFC 7617.
- self.assertEqual(
- self.fetch(
- "/auth", auth_username="Aladdin", auth_password="open sesame"
- ).body,
- b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
- )
- def test_basic_auth_explicit_mode(self):
- self.assertEqual(
- self.fetch(
- "/auth",
- auth_username="Aladdin",
- auth_password="open sesame",
- auth_mode="basic",
- ).body,
- b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
- )
- def test_basic_auth_unicode(self):
- # This test data appears in section 2.1 of RFC 7617.
- self.assertEqual(
- self.fetch("/auth", auth_username="test", auth_password="123£").body,
- b"Basic dGVzdDoxMjPCow==",
- )
- # The standard mandates NFC. Give it a decomposed username
- # and ensure it is normalized to composed form.
- username = unicodedata.normalize("NFD", u"josé")
- self.assertEqual(
- self.fetch("/auth", auth_username=username, auth_password="səcrət").body,
- b"Basic am9zw6k6c8mZY3LJmXQ=",
- )
- def test_unsupported_auth_mode(self):
- # curl and simple clients handle errors a bit differently; the
- # important thing is that they don't fall back to basic auth
- # on an unknown mode.
- with ExpectLog(gen_log, "uncaught exception", required=False):
- with self.assertRaises((ValueError, HTTPError)):
- self.fetch(
- "/auth",
- auth_username="Aladdin",
- auth_password="open sesame",
- auth_mode="asdf",
- raise_error=True,
- )
- def test_follow_redirect(self):
- response = self.fetch("/countdown/2", follow_redirects=False)
- self.assertEqual(302, response.code)
- self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
- response = self.fetch("/countdown/2")
- self.assertEqual(200, response.code)
- self.assertTrue(response.effective_url.endswith("/countdown/0"))
- self.assertEqual(b"Zero", response.body)
- def test_redirect_without_location(self):
- response = self.fetch("/redirect_without_location", follow_redirects=True)
- # If there is no location header, the redirect response should
- # just be returned as-is. (This should arguably raise an
- # error, but libcurl doesn't treat this as an error, so we
- # don't either).
- self.assertEqual(301, response.code)
- def test_redirect_put_with_body(self):
- response = self.fetch(
- "/redirect?url=/put&status=307", method="PUT", body="hello"
- )
- self.assertEqual(response.body, b"Put body: hello")
- def test_redirect_put_without_body(self):
- # This "without body" edge case is similar to what happens with body_producer.
- response = self.fetch(
- "/redirect?url=/put&status=307",
- method="PUT",
- allow_nonstandard_methods=True,
- )
- self.assertEqual(response.body, b"Put body: ")
- def test_method_after_redirect(self):
- # Legacy redirect codes (301, 302) convert POST requests to GET.
- for status in [301, 302, 303]:
- url = "/redirect?url=/all_methods&status=%d" % status
- resp = self.fetch(url, method="POST", body=b"")
- self.assertEqual(b"GET", resp.body)
- # Other methods are left alone.
- for method in ["GET", "OPTIONS", "PUT", "DELETE"]:
- resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
- self.assertEqual(utf8(method), resp.body)
- # HEAD is different so check it separately.
- resp = self.fetch(url, method="HEAD")
- self.assertEqual(200, resp.code)
- self.assertEqual(b"", resp.body)
- # Newer redirects always preserve the original method.
- for status in [307, 308]:
- url = "/redirect?url=/all_methods&status=307"
- for method in ["GET", "OPTIONS", "POST", "PUT", "DELETE"]:
- resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
- self.assertEqual(method, to_unicode(resp.body))
- resp = self.fetch(url, method="HEAD")
- self.assertEqual(200, resp.code)
- self.assertEqual(b"", resp.body)
- def test_credentials_in_url(self):
- url = self.get_url("/auth").replace("http://", "http://me:secret@")
- response = self.fetch(url)
- self.assertEqual(b"Basic " + base64.b64encode(b"me:secret"), response.body)
- def test_body_encoding(self):
- unicode_body = u"\xe9"
- byte_body = binascii.a2b_hex(b"e9")
- # unicode string in body gets converted to utf8
- response = self.fetch(
- "/echopost",
- method="POST",
- body=unicode_body,
- headers={"Content-Type": "application/blah"},
- )
- self.assertEqual(response.headers["Content-Length"], "2")
- self.assertEqual(response.body, utf8(unicode_body))
- # byte strings pass through directly
- response = self.fetch(
- "/echopost",
- method="POST",
- body=byte_body,
- headers={"Content-Type": "application/blah"},
- )
- self.assertEqual(response.headers["Content-Length"], "1")
- self.assertEqual(response.body, byte_body)
- # Mixing unicode in headers and byte string bodies shouldn't
- # break anything
- response = self.fetch(
- "/echopost",
- method="POST",
- body=byte_body,
- headers={"Content-Type": "application/blah"},
- user_agent=u"foo",
- )
- self.assertEqual(response.headers["Content-Length"], "1")
- self.assertEqual(response.body, byte_body)
- def test_types(self):
- response = self.fetch("/hello")
- self.assertEqual(type(response.body), bytes)
- self.assertEqual(type(response.headers["Content-Type"]), str)
- self.assertEqual(type(response.code), int)
- self.assertEqual(type(response.effective_url), str)
- def test_header_callback(self):
- first_line = []
- headers = {}
- chunks = []
- def header_callback(header_line):
- if header_line.startswith("HTTP/1.1 101"):
- # Upgrading to HTTP/2
- pass
- elif header_line.startswith("HTTP/"):
- first_line.append(header_line)
- elif header_line != "\r\n":
- k, v = header_line.split(":", 1)
- headers[k.lower()] = v.strip()
- def streaming_callback(chunk):
- # All header callbacks are run before any streaming callbacks,
- # so the header data is available to process the data as it
- # comes in.
- self.assertEqual(headers["content-type"], "text/html; charset=UTF-8")
- chunks.append(chunk)
- self.fetch(
- "/chunk",
- header_callback=header_callback,
- streaming_callback=streaming_callback,
- )
- self.assertEqual(len(first_line), 1, first_line)
- self.assertRegexpMatches(first_line[0], "HTTP/[0-9]\\.[0-9] 200.*\r\n")
- self.assertEqual(chunks, [b"asdf", b"qwer"])
- @gen_test
- def test_configure_defaults(self):
- defaults = dict(user_agent="TestDefaultUserAgent", allow_ipv6=False)
- # Construct a new instance of the configured client class
- client = self.http_client.__class__(force_instance=True, defaults=defaults)
- try:
- response = yield client.fetch(self.get_url("/user_agent"))
- self.assertEqual(response.body, b"TestDefaultUserAgent")
- finally:
- client.close()
- def test_header_types(self):
- # Header values may be passed as character or utf8 byte strings,
- # in a plain dictionary or an HTTPHeaders object.
- # Keys must always be the native str type.
- # All combinations should have the same results on the wire.
- for value in [u"MyUserAgent", b"MyUserAgent"]:
- for container in [dict, HTTPHeaders]:
- headers = container()
- headers["User-Agent"] = value
- resp = self.fetch("/user_agent", headers=headers)
- self.assertEqual(
- resp.body,
- b"MyUserAgent",
- "response=%r, value=%r, container=%r"
- % (resp.body, value, container),
- )
- def test_multi_line_headers(self):
- # Multi-line http headers are rare but rfc-allowed
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
- sock, port = bind_unused_port()
- with closing(sock):
- @gen.coroutine
- def accept_callback(conn, address):
- stream = IOStream(conn)
- request_data = yield stream.read_until(b"\r\n\r\n")
- if b"HTTP/1." not in request_data:
- self.skipTest("requires HTTP/1.x")
- yield stream.write(
- b"""\
- HTTP/1.1 200 OK
- X-XSS-Protection: 1;
- \tmode=block
- """.replace(
- b"\n", b"\r\n"
- )
- )
- stream.close()
- netutil.add_accept_handler(sock, accept_callback) # type: ignore
- resp = self.fetch("http://127.0.0.1:%d/" % port)
- resp.rethrow()
- self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block")
- self.io_loop.remove_handler(sock.fileno())
- def test_304_with_content_length(self):
- # According to the spec 304 responses SHOULD NOT include
- # Content-Length or other entity headers, but some servers do it
- # anyway.
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- response = self.fetch("/304_with_content_length")
- self.assertEqual(response.code, 304)
- self.assertEqual(response.headers["Content-Length"], "42")
- @gen_test
- def test_future_interface(self):
- response = yield self.http_client.fetch(self.get_url("/hello"))
- self.assertEqual(response.body, b"Hello world!")
- @gen_test
- def test_future_http_error(self):
- with self.assertRaises(HTTPError) as context:
- yield self.http_client.fetch(self.get_url("/notfound"))
- self.assertEqual(context.exception.code, 404)
- self.assertEqual(context.exception.response.code, 404)
- @gen_test
- def test_future_http_error_no_raise(self):
- response = yield self.http_client.fetch(
- self.get_url("/notfound"), raise_error=False
- )
- self.assertEqual(response.code, 404)
- @gen_test
- def test_reuse_request_from_response(self):
- # The response.request attribute should be an HTTPRequest, not
- # a _RequestProxy.
- # This test uses self.http_client.fetch because self.fetch calls
- # self.get_url on the input unconditionally.
- url = self.get_url("/hello")
- response = yield self.http_client.fetch(url)
- self.assertEqual(response.request.url, url)
- self.assertTrue(isinstance(response.request, HTTPRequest))
- response2 = yield self.http_client.fetch(response.request)
- self.assertEqual(response2.body, b"Hello world!")
- @gen_test
- def test_bind_source_ip(self):
- url = self.get_url("/hello")
- request = HTTPRequest(url, network_interface="127.0.0.1")
- response = yield self.http_client.fetch(request)
- self.assertEqual(response.code, 200)
- with self.assertRaises((ValueError, HTTPError)) as context:
- request = HTTPRequest(url, network_interface="not-interface-or-ip")
- yield self.http_client.fetch(request)
- self.assertIn("not-interface-or-ip", str(context.exception))
- def test_all_methods(self):
- for method in ["GET", "DELETE", "OPTIONS"]:
- response = self.fetch("/all_methods", method=method)
- self.assertEqual(response.body, utf8(method))
- for method in ["POST", "PUT", "PATCH"]:
- response = self.fetch("/all_methods", method=method, body=b"")
- self.assertEqual(response.body, utf8(method))
- response = self.fetch("/all_methods", method="HEAD")
- self.assertEqual(response.body, b"")
- response = self.fetch(
- "/all_methods", method="OTHER", allow_nonstandard_methods=True
- )
- self.assertEqual(response.body, b"OTHER")
- def test_body_sanity_checks(self):
- # These methods require a body.
- for method in ("POST", "PUT", "PATCH"):
- with self.assertRaises(ValueError) as context:
- self.fetch("/all_methods", method=method, raise_error=True)
- self.assertIn("must not be None", str(context.exception))
- resp = self.fetch(
- "/all_methods", method=method, allow_nonstandard_methods=True
- )
- self.assertEqual(resp.code, 200)
- # These methods don't allow a body.
- for method in ("GET", "DELETE", "OPTIONS"):
- with self.assertRaises(ValueError) as context:
- self.fetch(
- "/all_methods", method=method, body=b"asdf", raise_error=True
- )
- self.assertIn("must be None", str(context.exception))
- # In most cases this can be overridden, but curl_httpclient
- # does not allow body with a GET at all.
- if method != "GET":
- self.fetch(
- "/all_methods",
- method=method,
- body=b"asdf",
- allow_nonstandard_methods=True,
- raise_error=True,
- )
- self.assertEqual(resp.code, 200)
- # This test causes odd failures with the combination of
- # curl_httpclient (at least with the version of libcurl available
- # on ubuntu 12.04), TwistedIOLoop, and epoll. For POST (but not PUT),
- # curl decides the response came back too soon and closes the connection
- # to start again. It does this *before* telling the socket callback to
- # unregister the FD. Some IOLoop implementations have special kernel
- # integration to discover this immediately. Tornado's IOLoops
- # ignore errors on remove_handler to accommodate this behavior, but
- # Twisted's reactor does not. The removeReader call fails and so
- # do all future removeAll calls (which our tests do at cleanup).
- #
- # def test_post_307(self):
- # response = self.fetch("/redirect?status=307&url=/post",
- # method="POST", body=b"arg1=foo&arg2=bar")
- # self.assertEqual(response.body, b"Post arg1: foo, arg2: bar")
- def test_put_307(self):
- response = self.fetch(
- "/redirect?status=307&url=/put", method="PUT", body=b"hello"
- )
- response.rethrow()
- self.assertEqual(response.body, b"Put body: hello")
- def test_non_ascii_header(self):
- # Non-ascii headers are sent as latin1.
- response = self.fetch("/set_header?k=foo&v=%E9")
- response.rethrow()
- self.assertEqual(response.headers["Foo"], native_str(u"\u00e9"))
- def test_response_times(self):
- # A few simple sanity checks of the response time fields to
- # make sure they're using the right basis (between the
- # wall-time and monotonic clocks).
- start_time = time.time()
- response = self.fetch("/hello")
- response.rethrow()
- self.assertGreaterEqual(response.request_time, 0)
- self.assertLess(response.request_time, 1.0)
- # A very crude check to make sure that start_time is based on
- # wall time and not the monotonic clock.
- self.assertLess(abs(response.start_time - start_time), 1.0)
- for k, v in response.time_info.items():
- self.assertTrue(0 <= v < 1.0, "time_info[%s] out of bounds: %s" % (k, v))
- @gen_test
- def test_error_after_cancel(self):
- fut = self.http_client.fetch(self.get_url("/404"))
- self.assertTrue(fut.cancel())
- with ExpectLog(app_log, "Exception after Future was cancelled") as el:
- # We can't wait on the cancelled Future any more, so just
- # let the IOLoop run until the exception gets logged (or
- # not, in which case we exit the loop and ExpectLog will
- # raise).
- for i in range(100):
- yield gen.sleep(0.01)
- if el.logged_stack:
- break
- class RequestProxyTest(unittest.TestCase):
- def test_request_set(self):
- proxy = _RequestProxy(
- HTTPRequest("http://example.com/", user_agent="foo"), dict()
- )
- self.assertEqual(proxy.user_agent, "foo")
- def test_default_set(self):
- proxy = _RequestProxy(
- HTTPRequest("http://example.com/"), dict(network_interface="foo")
- )
- self.assertEqual(proxy.network_interface, "foo")
- def test_both_set(self):
- proxy = _RequestProxy(
- HTTPRequest("http://example.com/", proxy_host="foo"), dict(proxy_host="bar")
- )
- self.assertEqual(proxy.proxy_host, "foo")
- def test_neither_set(self):
- proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
- self.assertIs(proxy.auth_username, None)
- def test_bad_attribute(self):
- proxy = _RequestProxy(HTTPRequest("http://example.com/"), dict())
- with self.assertRaises(AttributeError):
- proxy.foo
- def test_defaults_none(self):
- proxy = _RequestProxy(HTTPRequest("http://example.com/"), None)
- self.assertIs(proxy.auth_username, None)
- class HTTPResponseTestCase(unittest.TestCase):
- def test_str(self):
- response = HTTPResponse( # type: ignore
- HTTPRequest("http://example.com"), 200, headers={}, buffer=BytesIO()
- )
- s = str(response)
- self.assertTrue(s.startswith("HTTPResponse("))
- self.assertIn("code=200", s)
- class SyncHTTPClientTest(unittest.TestCase):
- def setUp(self):
- self.server_ioloop = IOLoop()
- event = threading.Event()
- @gen.coroutine
- def init_server():
- sock, self.port = bind_unused_port()
- app = Application([("/", HelloWorldHandler)])
- self.server = HTTPServer(app)
- self.server.add_socket(sock)
- event.set()
- def start():
- self.server_ioloop.run_sync(init_server)
- self.server_ioloop.start()
- self.server_thread = threading.Thread(target=start)
- self.server_thread.start()
- event.wait()
- self.http_client = HTTPClient()
- def tearDown(self):
- def stop_server():
- self.server.stop()
- # Delay the shutdown of the IOLoop by several iterations because
- # the server may still have some cleanup work left when
- # the client finishes with the response (this is noticeable
- # with http/2, which leaves a Future with an unexamined
- # StreamClosedError on the loop).
- @gen.coroutine
- def slow_stop():
- yield self.server.close_all_connections()
- # The number of iterations is difficult to predict. Typically,
- # one is sufficient, although sometimes it needs more.
- for i in range(5):
- yield
- self.server_ioloop.stop()
- self.server_ioloop.add_callback(slow_stop)
- self.server_ioloop.add_callback(stop_server)
- self.server_thread.join()
- self.http_client.close()
- self.server_ioloop.close(all_fds=True)
- def get_url(self, path):
- return "http://127.0.0.1:%d%s" % (self.port, path)
- def test_sync_client(self):
- response = self.http_client.fetch(self.get_url("/"))
- self.assertEqual(b"Hello world!", response.body)
- def test_sync_client_error(self):
- # Synchronous HTTPClient raises errors directly; no need for
- # response.rethrow()
- with self.assertRaises(HTTPError) as assertion:
- self.http_client.fetch(self.get_url("/notfound"))
- self.assertEqual(assertion.exception.code, 404)
- class SyncHTTPClientSubprocessTest(unittest.TestCase):
- def test_destructor_log(self):
- # Regression test for
- # https://github.com/tornadoweb/tornado/issues/2539
- #
- # In the past, the following program would log an
- # "inconsistent AsyncHTTPClient cache" error from a destructor
- # when the process is shutting down. The shutdown process is
- # subtle and I don't fully understand it; the failure does not
- # manifest if that lambda isn't there or is a simpler object
- # like an int (nor does it manifest in the tornado test suite
- # as a whole, which is why we use this subprocess).
- proc = subprocess.run(
- [
- sys.executable,
- "-c",
- "from tornado.httpclient import HTTPClient; f = lambda: None; c = HTTPClient()",
- ],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- check=True,
- )
- if proc.stdout:
- print("STDOUT:")
- print(to_unicode(proc.stdout))
- if proc.stdout:
- self.fail("subprocess produced unexpected output")
- class HTTPRequestTestCase(unittest.TestCase):
- def test_headers(self):
- request = HTTPRequest("http://example.com", headers={"foo": "bar"})
- self.assertEqual(request.headers, {"foo": "bar"})
- def test_headers_setter(self):
- request = HTTPRequest("http://example.com")
- request.headers = {"bar": "baz"} # type: ignore
- self.assertEqual(request.headers, {"bar": "baz"})
- def test_null_headers_setter(self):
- request = HTTPRequest("http://example.com")
- request.headers = None # type: ignore
- self.assertEqual(request.headers, {})
- def test_body(self):
- request = HTTPRequest("http://example.com", body="foo")
- self.assertEqual(request.body, utf8("foo"))
- def test_body_setter(self):
- request = HTTPRequest("http://example.com")
- request.body = "foo" # type: ignore
- self.assertEqual(request.body, utf8("foo"))
- def test_if_modified_since(self):
- http_date = datetime.datetime.utcnow()
- request = HTTPRequest("http://example.com", if_modified_since=http_date)
- self.assertEqual(
- request.headers, {"If-Modified-Since": format_timestamp(http_date)}
- )
- class HTTPErrorTestCase(unittest.TestCase):
- def test_copy(self):
- e = HTTPError(403)
- e2 = copy.copy(e)
- self.assertIsNot(e, e2)
- self.assertEqual(e.code, e2.code)
- def test_plain_error(self):
- e = HTTPError(403)
- self.assertEqual(str(e), "HTTP 403: Forbidden")
- self.assertEqual(repr(e), "HTTP 403: Forbidden")
- def test_error_with_response(self):
- resp = HTTPResponse(HTTPRequest("http://example.com/"), 403)
- with self.assertRaises(HTTPError) as cm:
- resp.rethrow()
- e = cm.exception
- self.assertEqual(str(e), "HTTP 403: Forbidden")
- self.assertEqual(repr(e), "HTTP 403: Forbidden")
|