Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
simple_httpclient_test.py867 linesDownload Raw Back to test
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 
codekingpro/portable-devtools · Team Ai