codekingpro/portable-devtools
115k
1import collections
2from contextlib import closing
3import errno
4import logging
5import os
6import re
7import socket
8import ssl
9import sys
10import typing # noqa: F401
11
12from tornado.escape import to_unicode, utf8
13from tornado import gen, version
14from tornado.httpclient import AsyncHTTPClient, HTTPResponse
15from tornado.httpserver import HTTPServer
16from tornado.httputil import HTTPHeaders, ResponseStartLine
17from tornado.ioloop import IOLoop
18from tornado.iostream import UnsatisfiableReadError
19from tornado.locks import Event
20from tornado.log import gen_log
21from tornado.netutil import Resolver, bind_sockets
22from tornado.simple_httpclient import (
23 SimpleAsyncHTTPClient,
24 HTTPStreamClosedError,
25 HTTPTimeoutError,
26)
27from tornado.test.httpclient_test import (
28 ChunkHandler,
29 CountdownHandler,
30 HelloWorldHandler,
31 RedirectHandler,
32 UserAgentHandler,
33)
34from tornado.test import httpclient_test
35from tornado.testing import (
36 AsyncHTTPTestCase,
37 AsyncHTTPSTestCase,
38 AsyncTestCase,
39 ExpectLog,
40 gen_test,
41)
42from tornado.test.util import (
43 abstract_base_test,
44 skipIfNoIPv6,
45 refusing_port,
46)
47from tornado.web import RequestHandler, Application, url, stream_request_body
48
49
50class SimpleHTTPClientCommonTestCase(httpclient_test.HTTPClientCommonTestCase):
51 def get_http_client(self):
52 client = SimpleAsyncHTTPClient(force_instance=True)
53 self.assertTrue(isinstance(client, SimpleAsyncHTTPClient))
54 return client
55
56
57class TriggerHandler(RequestHandler):
58 def initialize(self, queue, wake_callback):
59 self.queue = queue
60 self.wake_callback = wake_callback
61
62 @gen.coroutine
63 def get(self):
64 logging.debug("queuing trigger")
65 event = Event()
66 self.queue.append(event.set)
67 if self.get_argument("wake", "true") == "true":
68 self.wake_callback()
69 yield event.wait()
70
71
72class ContentLengthHandler(RequestHandler):
73 def get(self):
74 self.stream = self.detach()
75 IOLoop.current().spawn_callback(self.write_response)
76
77 @gen.coroutine
78 def write_response(self):
79 yield self.stream.write(
80 utf8(
81 "HTTP/1.0 200 OK\r\nContent-Length: %s\r\n\r\nok"
82 % self.get_argument("value")
83 )
84 )
85 self.stream.close()
86
87
88class HeadHandler(RequestHandler):
89 def head(self):
90 self.set_header("Content-Length", "7")
91
92
93class OptionsHandler(RequestHandler):
94 def options(self):
95 self.set_header("Access-Control-Allow-Origin", "*")
96 self.write("ok")
97
98
99class NoContentHandler(RequestHandler):
100 def get(self):
101 self.set_status(204)
102 self.finish()
103
104
105class SeeOtherPostHandler(RequestHandler):
106 def post(self):
107 redirect_code = int(self.request.body)
108 assert redirect_code in (302, 303), "unexpected body %r" % self.request.body
109 self.set_header("Location", "/see_other_get")
110 self.set_status(redirect_code)
111
112
113class SeeOtherGetHandler(RequestHandler):
114 def get(self):
115 if self.request.body:
116 raise Exception("unexpected body %r" % self.request.body)
117 self.write("ok")
118
119
120class HostEchoHandler(RequestHandler):
121 def get(self):
122 self.write(self.request.headers["Host"])
123
124
125class NoContentLengthHandler(RequestHandler):
126 def get(self):
127 if self.request.version.startswith("HTTP/1"):
128 # Emulate the old HTTP/1.0 behavior of returning a body with no
129 # content-length. Tornado handles content-length at the framework
130 # level so we have to go around it.
131 stream = self.detach()
132 stream.write(b"HTTP/1.0 200 OK\r\n\r\n" b"hello")
133 stream.close()
134 else:
135 self.finish("HTTP/1 required")
136
137
138class EchoPostHandler(RequestHandler):
139 def post(self):
140 self.write(self.request.body)
141
142
143@stream_request_body
144class RespondInPrepareHandler(RequestHandler):
145 def prepare(self):
146 self.set_status(403)
147 self.finish("forbidden")
148
149
150@abstract_base_test
151class SimpleHTTPClientTestMixin(AsyncTestCase):
152 # See comments on TestIOStreamWebMixin
153 def get_http_port(self) -> int:
154 raise NotImplementedError()
155
156 def fetch(
157 self, path: str, raise_error: bool = False, **kwargs: typing.Any
158 ) -> HTTPResponse:
159 # To be filled in by mixing in AsyncHTTPTestCase or AsyncHTTPSTestCase
160 raise NotImplementedError()
161
162 def get_url(self, path: str) -> str:
163 raise NotImplementedError()
164
165 def get_protocol(self) -> str:
166 raise NotImplementedError()
167
168 def get_http_server(self) -> HTTPServer:
169 raise NotImplementedError()
170
171 def create_client(self, **kwargs):
172 raise NotImplementedError()
173
174 def mixin_get_app(self):
175 # callable objects to finish pending /trigger requests
176 self.triggers = (
177 collections.deque()
178 ) # type: typing.Deque[typing.Callable[[], None]]
179 return Application(
180 [
181 url(
182 "/trigger",
183 TriggerHandler,
184 dict(queue=self.triggers, wake_callback=self.stop),
185 ),
186 url("/chunk", ChunkHandler),
187 url("/countdown/([0-9]+)", CountdownHandler, name="countdown"),
188 url("/hello", HelloWorldHandler),
189 url("/content_length", ContentLengthHandler),
190 url("/head", HeadHandler),
191 url("/options", OptionsHandler),
192 url("/no_content", NoContentHandler),
193 url("/see_other_post", SeeOtherPostHandler),
194 url("/see_other_get", SeeOtherGetHandler),
195 url("/host_echo", HostEchoHandler),
196 url("/no_content_length", NoContentLengthHandler),
197 url("/echo_post", EchoPostHandler),
198 url("/respond_in_prepare", RespondInPrepareHandler),
199 url("/redirect", RedirectHandler),
200 url("/user_agent", UserAgentHandler),
201 ],
202 gzip=True,
203 )
204
205 def test_singleton(self):
206 # Class "constructor" reuses objects on the same IOLoop
207 self.assertIs(SimpleAsyncHTTPClient(), SimpleAsyncHTTPClient())
208 # unless force_instance is used
209 self.assertIsNot(
210 SimpleAsyncHTTPClient(), SimpleAsyncHTTPClient(force_instance=True)
211 )
212 # different IOLoops use different objects
213 with closing(IOLoop(make_current=False)) as io_loop2:
214
215 async def make_client():
216 await gen.sleep(0)
217 return SimpleAsyncHTTPClient()
218
219 client1 = self.io_loop.run_sync(make_client)
220 client2 = io_loop2.run_sync(make_client)
221 self.assertIsNot(client1, client2)
222
223 def test_connection_limit(self):
224 with closing(self.create_client(max_clients=2)) as client:
225 self.assertEqual(client.max_clients, 2)
226 seen = []
227 # Send 4 requests. Two can be sent immediately, while the others
228 # will be queued
229 for i in range(4):
230
231 def cb(fut, i=i):
232 seen.append(i)
233 self.stop()
234
235 client.fetch(self.get_url("/trigger")).add_done_callback(cb)
236 self.wait(condition=lambda: len(self.triggers) == 2)
237 self.assertEqual(len(client.queue), 2)
238
239 # Finish the first two requests and let the next two through
240 self.triggers.popleft()()
241 self.triggers.popleft()()
242 self.wait(condition=lambda: (len(self.triggers) == 2 and len(seen) == 2))
243 self.assertEqual(set(seen), {0, 1})
244 self.assertEqual(len(client.queue), 0)
245
246 # Finish all the pending requests
247 self.triggers.popleft()()
248 self.triggers.popleft()()
249 self.wait(condition=lambda: len(seen) == 4)
250 self.assertEqual(set(seen), {0, 1, 2, 3})
251 self.assertEqual(len(self.triggers), 0)
252
253 @gen_test
254 def test_redirect_connection_limit(self):
255 # following redirects should not consume additional connections
256 with closing(self.create_client(max_clients=1)) as client:
257 response = yield client.fetch(self.get_url("/countdown/3"), max_redirects=3)
258 response.rethrow()
259
260 def test_max_redirects(self):
261 response = self.fetch("/countdown/5", max_redirects=3)
262 self.assertEqual(302, response.code)
263 # We requested 5, followed three redirects for 4, 3, 2, then the last
264 # unfollowed redirect is to 1.
265 self.assertTrue(response.request.url.endswith("/countdown/5"))
266 self.assertTrue(response.effective_url.endswith("/countdown/2"))
267 self.assertTrue(response.headers["Location"].endswith("/countdown/1"))
268
269 def test_header_reuse(self):
270 # Apps may reuse a headers object if they are only passing in constant
271 # headers like user-agent. The header object should not be modified.
272 headers = HTTPHeaders({"User-Agent": "Foo"})
273 self.fetch("/hello", headers=headers)
274 self.assertEqual(list(headers.get_all()), [("User-Agent", "Foo")])
275
276 def test_default_user_agent(self):
277 response = self.fetch("/user_agent", method="GET")
278 self.assertEqual(200, response.code)
279 self.assertEqual(response.body.decode(), f"Tornado/{version}")
280
281 def test_see_other_redirect(self):
282 for code in (302, 303):
283 response = self.fetch("/see_other_post", method="POST", body="%d" % code)
284 self.assertEqual(200, response.code)
285 self.assertTrue(response.request.url.endswith("/see_other_post"))
286 self.assertTrue(response.effective_url.endswith("/see_other_get"))
287 # request is the original request, is a POST still
288 self.assertEqual("POST", response.request.method)
289
290 @gen_test
291 def test_connect_timeout(self):
292 timeout = 0.1
293
294 cleanup_event = Event()
295 test = self
296
297 class TimeoutResolver(Resolver):
298 async def resolve(self, *args, **kwargs):
299 await cleanup_event.wait()
300 # Return something valid so the test doesn't raise during shutdown.
301 return [(socket.AF_INET, ("127.0.0.1", test.get_http_port()))]
302
303 with closing(self.create_client(resolver=TimeoutResolver())) as client:
304 with self.assertRaises(HTTPTimeoutError):
305 yield client.fetch(
306 self.get_url("/hello"),
307 connect_timeout=timeout,
308 request_timeout=3600,
309 raise_error=True,
310 )
311
312 # Let the hanging coroutine clean up after itself. We need to
313 # wait more than a single IOLoop iteration for the SSL case,
314 # which logs errors on unexpected EOF.
315 cleanup_event.set()
316 yield gen.sleep(0.2)
317
318 def test_request_timeout(self):
319 timeout = 0.1
320 if os.name == "nt":
321 timeout = 0.5
322
323 with self.assertRaises(HTTPTimeoutError):
324 self.fetch("/trigger?wake=false", request_timeout=timeout, raise_error=True)
325 # trigger the hanging request to let it clean up after itself
326 self.triggers.popleft()()
327 self.io_loop.run_sync(lambda: gen.sleep(0))
328
329 @skipIfNoIPv6
330 def test_ipv6(self):
331 [sock] = bind_sockets(0, "::1", family=socket.AF_INET6)
332 port = sock.getsockname()[1]
333 self.get_http_server().add_socket(sock)
334 url = "%s://[::1]:%d/hello" % (self.get_protocol(), port)
335
336 # ipv6 is currently enabled by default but can be disabled
337 with self.assertRaises(Exception):
338 self.fetch(url, allow_ipv6=False, raise_error=True)
339
340 response = self.fetch(url)
341 self.assertEqual(response.body, b"Hello world!")
342
343 def test_multiple_content_length_accepted(self):
344 response = self.fetch("/content_length?value=2,2")
345 self.assertEqual(response.body, b"ok")
346 response = self.fetch("/content_length?value=2,%202,2")
347 self.assertEqual(response.body, b"ok")
348
349 with ExpectLog(
350 gen_log, ".*Multiple unequal Content-Lengths", level=logging.INFO
351 ):
352 with self.assertRaises(HTTPStreamClosedError):
353 self.fetch("/content_length?value=2,4", raise_error=True)
354 with self.assertRaises(HTTPStreamClosedError):
355 self.fetch("/content_length?value=2,%202,3", raise_error=True)
356
357 def test_head_request(self):
358 response = self.fetch("/head", method="HEAD")
359 self.assertEqual(response.code, 200)
360 self.assertEqual(response.headers["content-length"], "7")
361 self.assertFalse(response.body)
362
363 def test_options_request(self):
364 response = self.fetch("/options", method="OPTIONS")
365 self.assertEqual(response.code, 200)
366 self.assertEqual(response.headers["content-length"], "2")
367 self.assertEqual(response.headers["access-control-allow-origin"], "*")
368 self.assertEqual(response.body, b"ok")
369
370 def test_no_content(self):
371 response = self.fetch("/no_content")
372 self.assertEqual(response.code, 204)
373 # 204 status shouldn't have a content-length
374 #
375 # Tests with a content-length header are included below
376 # in HTTP204NoContentTestCase.
377 self.assertNotIn("Content-Length", response.headers)
378
379 def test_host_header(self):
380 host_re = re.compile(b"^127.0.0.1:[0-9]+$")
381 response = self.fetch("/host_echo")
382 self.assertTrue(host_re.match(response.body))
383
384 url = self.get_url("/host_echo").replace("http://", "http://me:secret@")
385 response = self.fetch(url)
386 self.assertTrue(host_re.match(response.body), response.body)
387
388 def test_connection_refused(self):
389 cleanup_func, port = refusing_port()
390 self.addCleanup(cleanup_func)
391 with ExpectLog(gen_log, ".*", required=False):
392 with self.assertRaises(socket.error) as cm:
393 self.fetch("http://127.0.0.1:%d/" % port, raise_error=True)
394
395 if sys.platform != "cygwin":
396 # cygwin returns EPERM instead of ECONNREFUSED here
397 contains_errno = str(errno.ECONNREFUSED) in str(cm.exception)
398 if not contains_errno and hasattr(errno, "WSAECONNREFUSED"):
399 contains_errno = str(errno.WSAECONNREFUSED) in str( # type: ignore
400 cm.exception
401 )
402 self.assertTrue(contains_errno, cm.exception)
403 # This is usually "Connection refused".
404 # On windows, strerror is broken and returns "Unknown error".
405 expected_message = os.strerror(errno.ECONNREFUSED)
406 self.assertTrue(expected_message in str(cm.exception), cm.exception)
407
408 def test_queue_timeout(self):
409 with closing(self.create_client(max_clients=1)) as client:
410 # Wait for the trigger request to block, not complete.
411 fut1 = client.fetch(self.get_url("/trigger"), request_timeout=10)
412 self.wait()
413 with self.assertRaises(HTTPTimeoutError) as cm:
414 self.io_loop.run_sync(
415 lambda: client.fetch(
416 self.get_url("/hello"), connect_timeout=0.1, raise_error=True
417 )
418 )
419
420 self.assertEqual(str(cm.exception), "Timeout in request queue")
421 self.triggers.popleft()()
422 self.io_loop.run_sync(lambda: fut1)
423
424 def test_no_content_length(self):
425 response = self.fetch("/no_content_length")
426 if response.body == b"HTTP/1 required":
427 self.skipTest("requires HTTP/1.x")
428 else:
429 self.assertEqual(b"hello", response.body)
430
431 def sync_body_producer(self, write):
432 write(b"1234")
433 write(b"5678")
434
435 @gen.coroutine
436 def async_body_producer(self, write):
437 yield write(b"1234")
438 yield gen.moment
439 yield write(b"5678")
440
441 def test_sync_body_producer_chunked(self):
442 response = self.fetch(
443 "/echo_post", method="POST", body_producer=self.sync_body_producer
444 )
445 response.rethrow()
446 self.assertEqual(response.body, b"12345678")
447
448 def test_sync_body_producer_content_length(self):
449 response = self.fetch(
450 "/echo_post",
451 method="POST",
452 body_producer=self.sync_body_producer,
453 headers={"Content-Length": "8"},
454 )
455 response.rethrow()
456 self.assertEqual(response.body, b"12345678")
457
458 def test_async_body_producer_chunked(self):
459 response = self.fetch(
460 "/echo_post", method="POST", body_producer=self.async_body_producer
461 )
462 response.rethrow()
463 self.assertEqual(response.body, b"12345678")
464
465 def test_async_body_producer_content_length(self):
466 response = self.fetch(
467 "/echo_post",
468 method="POST",
469 body_producer=self.async_body_producer,
470 headers={"Content-Length": "8"},
471 )
472 response.rethrow()
473 self.assertEqual(response.body, b"12345678")
474
475 def test_native_body_producer_chunked(self):
476 async def body_producer(write):
477 await write(b"1234")
478 import asyncio
479
480 await asyncio.sleep(0)
481 await write(b"5678")
482
483 response = self.fetch("/echo_post", method="POST", body_producer=body_producer)
484 response.rethrow()
485 self.assertEqual(response.body, b"12345678")
486
487 def test_native_body_producer_content_length(self):
488 async def body_producer(write):
489 await write(b"1234")
490 import asyncio
491
492 await asyncio.sleep(0)
493 await write(b"5678")
494
495 response = self.fetch(
496 "/echo_post",
497 method="POST",
498 body_producer=body_producer,
499 headers={"Content-Length": "8"},
500 )
501 response.rethrow()
502 self.assertEqual(response.body, b"12345678")
503
504 def test_100_continue(self):
505 response = self.fetch(
506 "/echo_post", method="POST", body=b"1234", expect_100_continue=True
507 )
508 self.assertEqual(response.body, b"1234")
509
510 def test_100_continue_early_response(self):
511 def body_producer(write):
512 raise Exception("should not be called")
513
514 response = self.fetch(
515 "/respond_in_prepare",
516 method="POST",
517 body_producer=body_producer,
518 expect_100_continue=True,
519 )
520 self.assertEqual(response.code, 403)
521
522 def test_streaming_follow_redirects(self):
523 # When following redirects, header and streaming callbacks
524 # should only be called for the final result.
525 # TODO(bdarnell): this test belongs in httpclient_test instead of
526 # simple_httpclient_test, but it fails with the version of libcurl
527 # available on travis-ci. Move it when that has been upgraded
528 # or we have a better framework to skip tests based on curl version.
529 headers = [] # type: typing.List[str]
530 chunk_bytes = [] # type: typing.List[bytes]
531 self.fetch(
532 "/redirect?url=/hello",
533 header_callback=headers.append,
534 streaming_callback=chunk_bytes.append,
535 )
536 chunks = list(map(to_unicode, chunk_bytes))
537 self.assertEqual(chunks, ["Hello world!"])
538 # Make sure we only got one set of headers.
539 num_start_lines = len([h for h in headers if h.startswith("HTTP/")])
540 self.assertEqual(num_start_lines, 1)
541
542
543class SimpleHTTPClientTestCase(AsyncHTTPTestCase, SimpleHTTPClientTestMixin):
544 def setUp(self):
545 super().setUp()
546 self.http_client = self.create_client()
547
548 def get_app(self):
549 return self.mixin_get_app()
550
551 def create_client(self, **kwargs):
552 return SimpleAsyncHTTPClient(force_instance=True, **kwargs)
553
554
555class SimpleHTTPSClientTestCase(AsyncHTTPSTestCase, SimpleHTTPClientTestMixin):
556 def setUp(self):
557 super().setUp()
558 self.http_client = self.create_client()
559
560 def get_app(self):
561 return self.mixin_get_app()
562
563 def create_client(self, **kwargs):
564 return SimpleAsyncHTTPClient(
565 force_instance=True, defaults=dict(validate_cert=False), **kwargs
566 )
567
568 def test_ssl_options(self):
569 resp = self.fetch("/hello", ssl_options={"cert_reqs": ssl.CERT_NONE})
570 self.assertEqual(resp.body, b"Hello world!")
571
572 def test_ssl_context(self):
573 ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
574 ssl_ctx.check_hostname = False
575 ssl_ctx.verify_mode = ssl.CERT_NONE
576 resp = self.fetch("/hello", ssl_options=ssl_ctx)
577 self.assertEqual(resp.body, b"Hello world!")
578
579 def test_ssl_options_handshake_fail(self):
580 with ExpectLog(gen_log, "SSL Error|Uncaught exception", required=False):
581 with self.assertRaises(ssl.SSLError):
582 self.fetch(
583 "/hello",
584 ssl_options=dict(cert_reqs=ssl.CERT_REQUIRED),
585 raise_error=True,
586 )
587
588 def test_ssl_context_handshake_fail(self):
589 with ExpectLog(gen_log, "SSL Error|Uncaught exception"):
590 # CERT_REQUIRED is set by default.
591 ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
592 with self.assertRaises(ssl.SSLError):
593 self.fetch("/hello", ssl_options=ctx, raise_error=True)
594
595 def test_error_logging(self):
596 # No stack traces are logged for SSL errors (in this case,
597 # failure to validate the testing self-signed cert).
598 # The SSLError is exposed through ssl.SSLError.
599 with ExpectLog(gen_log, ".*") as expect_log:
600 with self.assertRaises(ssl.SSLError):
601 self.fetch("/", validate_cert=True, raise_error=True)
602 self.assertFalse(expect_log.logged_stack)
603
604
605class CreateAsyncHTTPClientTestCase(AsyncTestCase):
606 def setUp(self):
607 super().setUp()
608 self.saved = AsyncHTTPClient._save_configuration()
609
610 def tearDown(self):
611 AsyncHTTPClient._restore_configuration(self.saved)
612 super().tearDown()
613
614 def test_max_clients(self):
615 AsyncHTTPClient.configure(SimpleAsyncHTTPClient)
616 with closing(AsyncHTTPClient(force_instance=True)) as client:
617 self.assertEqual(client.max_clients, 10) # type: ignore
618 with closing(AsyncHTTPClient(max_clients=11, force_instance=True)) as client:
619 self.assertEqual(client.max_clients, 11) # type: ignore
620
621 # Now configure max_clients statically and try overriding it
622 # with each way max_clients can be passed
623 AsyncHTTPClient.configure(SimpleAsyncHTTPClient, max_clients=12)
624 with closing(AsyncHTTPClient(force_instance=True)) as client:
625 self.assertEqual(client.max_clients, 12) # type: ignore
626 with closing(AsyncHTTPClient(max_clients=13, force_instance=True)) as client:
627 self.assertEqual(client.max_clients, 13) # type: ignore
628 with closing(AsyncHTTPClient(max_clients=14, force_instance=True)) as client:
629 self.assertEqual(client.max_clients, 14) # type: ignore
630
631
632class HTTP100ContinueTestCase(AsyncHTTPTestCase):
633 def respond_100(self, request):
634 self.http1 = request.version.startswith("HTTP/1.")
635 if not self.http1:
636 request.connection.write_headers(
637 ResponseStartLine("", 200, "OK"), HTTPHeaders()
638 )
639 request.connection.finish()
640 return
641 self.request = request
642 fut = self.request.connection.stream.write(b"HTTP/1.1 100 CONTINUE\r\n\r\n")
643 fut.add_done_callback(self.respond_200)
644
645 def respond_200(self, fut):
646 fut.result()
647 fut = self.request.connection.stream.write(
648 b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nA"
649 )
650 fut.add_done_callback(lambda f: self.request.connection.stream.close())
651
652 def get_app(self):
653 # Not a full Application, but works as an HTTPServer callback
654 return self.respond_100
655
656 def test_100_continue(self):
657 res = self.fetch("/")
658 if not self.http1:
659 self.skipTest("requires HTTP/1.x")
660 self.assertEqual(res.body, b"A")
661
662
663class HTTP204NoContentTestCase(AsyncHTTPTestCase):
664 def respond_204(self, request):
665 self.http1 = request.version.startswith("HTTP/1.")
666 if not self.http1:
667 # Close the request cleanly in HTTP/2; it will be skipped anyway.
668 request.connection.write_headers(
669 ResponseStartLine("", 200, "OK"), HTTPHeaders()
670 )
671 request.connection.finish()
672 return
673
674 # A 204 response never has a body, even if doesn't have a content-length
675 # (which would otherwise mean read-until-close). We simulate here a
676 # server that sends no content length and does not close the connection.
677 #
678 # Tests of a 204 response with no Content-Length header are included
679 # in SimpleHTTPClientTestMixin.
680 stream = request.connection.detach()
681 stream.write(b"HTTP/1.1 204 No content\r\n")
682 if request.arguments.get("error", [False])[-1]:
683 stream.write(b"Content-Length: 5\r\n")
684 else:
685 stream.write(b"Content-Length: 0\r\n")
686 stream.write(b"\r\n")
687 stream.close()
688
689 def get_app(self):
690 return self.respond_204
691
692 def test_204_no_content(self):
693 resp = self.fetch("/")
694 if not self.http1:
695 self.skipTest("requires HTTP/1.x")
696 self.assertEqual(resp.code, 204)
697 self.assertEqual(resp.body, b"")
698
699 def test_204_invalid_content_length(self):
700 # 204 status with non-zero content length is malformed
701 with ExpectLog(
702 gen_log, ".*Response with code 204 should not have body", level=logging.INFO
703 ):
704 with self.assertRaises(HTTPStreamClosedError):
705 self.fetch("/?error=1", raise_error=True)
706 if not self.http1:
707 self.skipTest("requires HTTP/1.x")
708 if self.http_client.configured_class != SimpleAsyncHTTPClient:
709 self.skipTest("curl client accepts invalid headers")
710
711
712class HostnameMappingTestCase(AsyncHTTPTestCase):
713 def setUp(self):
714 super().setUp()
715 self.http_client = SimpleAsyncHTTPClient(
716 hostname_mapping={
717 "www.example.com": "127.0.0.1",
718 ("foo.example.com", 8000): ("127.0.0.1", self.get_http_port()),
719 }
720 )
721
722 def get_app(self):
723 return Application([url("/hello", HelloWorldHandler)])
724
725 def test_hostname_mapping(self):
726 response = self.fetch("http://www.example.com:%d/hello" % self.get_http_port())
727 response.rethrow()
728 self.assertEqual(response.body, b"Hello world!")
729
730 def test_port_mapping(self):
731 response = self.fetch("http://foo.example.com:8000/hello")
732 response.rethrow()
733 self.assertEqual(response.body, b"Hello world!")
734
735
736class ResolveTimeoutTestCase(AsyncHTTPTestCase):
737 def setUp(self):
738 self.cleanup_event = Event()
739 test = self
740
741 # Dummy Resolver subclass that never finishes.
742 class BadResolver(Resolver):
743 @gen.coroutine
744 def resolve(self, *args, **kwargs):
745 yield test.cleanup_event.wait()
746 # Return something valid so the test doesn't raise during cleanup.
747 return [(socket.AF_INET, ("127.0.0.1", test.get_http_port()))]
748
749 super().setUp()
750 self.http_client = SimpleAsyncHTTPClient(resolver=BadResolver())
751
752 def get_app(self):
753 return Application([url("/hello", HelloWorldHandler)])
754
755 def test_resolve_timeout(self):
756 with self.assertRaises(HTTPTimeoutError):
757 self.fetch("/hello", connect_timeout=0.1, raise_error=True)
758
759 # Let the hanging coroutine clean up after itself
760 self.cleanup_event.set()
761 self.io_loop.run_sync(lambda: gen.sleep(0))
762
763
764class MaxHeaderSizeTest(AsyncHTTPTestCase):
765 def get_app(self):
766 class SmallHeaders(RequestHandler):
767 def get(self):
768 self.set_header("X-Filler", "a" * 100)
769 self.write("ok")
770
771 class LargeHeaders(RequestHandler):
772 def get(self):
773 self.set_header("X-Filler", "a" * 1000)
774 self.write("ok")
775
776 return Application([("/small", SmallHeaders), ("/large", LargeHeaders)])
777
778 def get_http_client(self):
779 return SimpleAsyncHTTPClient(max_header_size=1024)
780
781 def test_small_headers(self):
782 response = self.fetch("/small")
783 response.rethrow()
784 self.assertEqual(response.body, b"ok")
785
786 def test_large_headers(self):
787 with ExpectLog(gen_log, "Unsatisfiable read", level=logging.INFO):
788 with self.assertRaises(UnsatisfiableReadError):
789 self.fetch("/large", raise_error=True)
790
791
792class MaxBodySizeTest(AsyncHTTPTestCase):
793 def get_app(self):
794 class SmallBody(RequestHandler):
795 def get(self):
796 self.write("a" * 1024 * 64)
797
798 class LargeBody(RequestHandler):
799 def get(self):
800 self.write("a" * 1024 * 100)
801
802 return Application([("/small", SmallBody), ("/large", LargeBody)])
803
804 def get_http_client(self):
805 return SimpleAsyncHTTPClient(max_body_size=1024 * 64)
806
807 def test_small_body(self):
808 response = self.fetch("/small")
809 response.rethrow()
810 self.assertEqual(response.body, b"a" * 1024 * 64)
811
812 def test_large_body(self):
813 with ExpectLog(
814 gen_log,
815 "Malformed HTTP message from None: Content-Length too long",
816 level=logging.INFO,
817 ):
818 with self.assertRaises(HTTPStreamClosedError):
819 self.fetch("/large", raise_error=True)
820
821
822class MaxBufferSizeTest(AsyncHTTPTestCase):
823 def get_app(self):
824 class LargeBody(RequestHandler):
825 def get(self):
826 self.write("a" * 1024 * 100)
827
828 return Application([("/large", LargeBody)])
829
830 def get_http_client(self):
831 # 100KB body with 64KB buffer
832 return SimpleAsyncHTTPClient(
833 max_body_size=1024 * 100, max_buffer_size=1024 * 64
834 )
835
836 def test_large_body(self):
837 response = self.fetch("/large")
838 response.rethrow()
839 self.assertEqual(response.body, b"a" * 1024 * 100)
840
841
842class ChunkedWithContentLengthTest(AsyncHTTPTestCase):
843 def get_app(self):
844 class ChunkedWithContentLength(RequestHandler):
845 def get(self):
846 # Add an invalid Transfer-Encoding to the response
847 self.set_header("Transfer-Encoding", "chunked")
848 self.write("Hello world")
849
850 return Application([("/chunkwithcl", ChunkedWithContentLength)])
851
852 def get_http_client(self):
853 return SimpleAsyncHTTPClient()
854
855 def test_chunked_with_content_length(self):
856 # Make sure the invalid headers are detected
857 with ExpectLog(
858 gen_log,
859 (
860 "Malformed HTTP message from None: Message "
861 "with both Transfer-Encoding and Content-Length"
862 ),
863 level=logging.INFO,
864 ):
865 with self.assertRaises(HTTPStreamClosedError):
866 self.fetch("/chunkwithcl", raise_error=True)
867 