| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- #
- # Copyright 2009 Facebook
- #
- # Licensed under the Apache License, Version 2.0 (the "License"); you may
- # not use this file except in compliance with the License. You may obtain
- # a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- # License for the specific language governing permissions and limitations
- # under the License.
- """WSGI support for the Tornado web framework.
- WSGI is the Python standard for web servers, and allows for interoperability
- between Tornado and other Python web frameworks and servers.
- This module provides WSGI support via the `WSGIContainer` class, which
- makes it possible to run applications using other WSGI frameworks on
- the Tornado HTTP server. The reverse is not supported; the Tornado
- `.Application` and `.RequestHandler` classes are designed for use with
- the Tornado `.HTTPServer` and cannot be used in a generic WSGI
- container.
- """
- import concurrent.futures
- from io import BytesIO
- import tornado
- import sys
- from tornado.concurrent import dummy_executor
- from tornado import escape
- from tornado import httputil
- from tornado.ioloop import IOLoop
- from tornado.log import access_log
- from typing import List, Tuple, Optional, Callable, Any, Dict
- from types import TracebackType
- import typing
- if typing.TYPE_CHECKING:
- from typing import Type # noqa: F401
- from _typeshed.wsgi import WSGIApplication as WSGIAppType # noqa: F401
- # PEP 3333 specifies that WSGI on python 3 generally deals with byte strings
- # that are smuggled inside objects of type unicode (via the latin1 encoding).
- # This function is like those in the tornado.escape module, but defined
- # here to minimize the temptation to use it in non-wsgi contexts.
- def to_wsgi_str(s: bytes) -> str:
- assert isinstance(s, bytes)
- return s.decode("latin1")
- class WSGIContainer:
- r"""Makes a WSGI-compatible application runnable on Tornado's HTTP server.
- .. warning::
- WSGI is a *synchronous* interface, while Tornado's concurrency model
- is based on single-threaded *asynchronous* execution. Many of Tornado's
- distinguishing features are not available in WSGI mode, including efficient
- long-polling and websockets. The primary purpose of `WSGIContainer` is
- to support both WSGI applications and native Tornado ``RequestHandlers`` in
- a single process. WSGI-only applications are likely to be better off
- with a dedicated WSGI server such as ``gunicorn`` or ``uwsgi``.
- Wrap a WSGI application in a `WSGIContainer` to make it implement the Tornado
- `.HTTPServer` ``request_callback`` interface. The `WSGIContainer` object can
- then be passed to classes from the `tornado.routing` module,
- `tornado.web.FallbackHandler`, or to `.HTTPServer` directly.
- This class is intended to let other frameworks (Django, Flask, etc)
- run on the Tornado HTTP server and I/O loop.
- Realistic usage will be more complicated, but the simplest possible example uses a
- hand-written WSGI application with `.HTTPServer`::
- def simple_app(environ, start_response):
- status = "200 OK"
- response_headers = [("Content-type", "text/plain")]
- start_response(status, response_headers)
- return [b"Hello world!\n"]
- async def main():
- container = tornado.wsgi.WSGIContainer(simple_app)
- http_server = tornado.httpserver.HTTPServer(container)
- http_server.listen(8888)
- await asyncio.Event().wait()
- asyncio.run(main())
- The recommended pattern is to use the `tornado.routing` module to set up routing
- rules between your WSGI application and, typically, a `tornado.web.Application`.
- Alternatively, `tornado.web.Application` can be used as the top-level router
- and `tornado.web.FallbackHandler` can embed a `WSGIContainer` within it.
- If the ``executor`` argument is provided, the WSGI application will be executed
- on that executor. This must be an instance of `concurrent.futures.Executor`,
- typically a ``ThreadPoolExecutor`` (``ProcessPoolExecutor`` is not supported).
- If no ``executor`` is given, the application will run on the event loop thread in
- Tornado 6.3; this will change to use an internal thread pool by default in
- Tornado 7.0.
- .. warning::
- By default, the WSGI application is executed on the event loop's thread. This
- limits the server to one request at a time (per process), making it less scalable
- than most other WSGI servers. It is therefore highly recommended that you pass
- a ``ThreadPoolExecutor`` when constructing the `WSGIContainer`, after verifying
- that your application is thread-safe. The default will change to use a
- ``ThreadPoolExecutor`` in Tornado 7.0.
- .. versionadded:: 6.3
- The ``executor`` parameter.
- .. deprecated:: 6.3
- The default behavior of running the WSGI application on the event loop thread
- is deprecated and will change in Tornado 7.0 to use a thread pool by default.
- """
- def __init__(
- self,
- wsgi_application: "WSGIAppType",
- executor: Optional[concurrent.futures.Executor] = None,
- ) -> None:
- self.wsgi_application = wsgi_application
- self.executor = dummy_executor if executor is None else executor
- def __call__(self, request: httputil.HTTPServerRequest) -> None:
- IOLoop.current().spawn_callback(self.handle_request, request)
- async def handle_request(self, request: httputil.HTTPServerRequest) -> None:
- data = {} # type: Dict[str, Any]
- response = [] # type: List[bytes]
- def start_response(
- status: str,
- headers: List[Tuple[str, str]],
- exc_info: Optional[
- Tuple[
- "Optional[Type[BaseException]]",
- Optional[BaseException],
- Optional[TracebackType],
- ]
- ] = None,
- ) -> Callable[[bytes], Any]:
- data["status"] = status
- data["headers"] = headers
- return response.append
- loop = IOLoop.current()
- app_response = await loop.run_in_executor(
- self.executor,
- self.wsgi_application,
- self.environ(request),
- start_response,
- )
- try:
- app_response_iter = iter(app_response)
- def next_chunk() -> Optional[bytes]:
- try:
- return next(app_response_iter)
- except StopIteration:
- # StopIteration is special and is not allowed to pass through
- # coroutines normally.
- return None
- while True:
- chunk = await loop.run_in_executor(self.executor, next_chunk)
- if chunk is None:
- break
- response.append(chunk)
- finally:
- if hasattr(app_response, "close"):
- app_response.close() # type: ignore
- body = b"".join(response)
- if not data:
- raise Exception("WSGI app did not call start_response")
- status_code_str, reason = data["status"].split(" ", 1)
- status_code = int(status_code_str)
- headers = data["headers"] # type: List[Tuple[str, str]]
- header_set = {k.lower() for (k, v) in headers}
- body = escape.utf8(body)
- if status_code != 304:
- if "content-length" not in header_set:
- headers.append(("Content-Length", str(len(body))))
- if "content-type" not in header_set:
- headers.append(("Content-Type", "text/html; charset=UTF-8"))
- if "server" not in header_set:
- headers.append(("Server", "TornadoServer/%s" % tornado.version))
- start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
- header_obj = httputil.HTTPHeaders()
- for key, value in headers:
- header_obj.add(key, value)
- assert request.connection is not None
- request.connection.write_headers(start_line, header_obj, chunk=body)
- request.connection.finish()
- self._log(status_code, request)
- def environ(self, request: httputil.HTTPServerRequest) -> Dict[str, Any]:
- """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
- .. versionchanged:: 6.3
- No longer a static method.
- """
- hostport = request.host.split(":")
- if len(hostport) == 2:
- host = hostport[0]
- port = int(hostport[1])
- else:
- host = request.host
- port = 443 if request.protocol == "https" else 80
- environ = {
- "REQUEST_METHOD": request.method,
- "SCRIPT_NAME": "",
- "PATH_INFO": to_wsgi_str(
- escape.url_unescape(request.path, encoding=None, plus=False)
- ),
- "QUERY_STRING": request.query,
- "REMOTE_ADDR": request.remote_ip,
- "SERVER_NAME": host,
- "SERVER_PORT": str(port),
- "SERVER_PROTOCOL": request.version,
- "wsgi.version": (1, 0),
- "wsgi.url_scheme": request.protocol,
- "wsgi.input": BytesIO(escape.utf8(request.body)),
- "wsgi.errors": sys.stderr,
- "wsgi.multithread": self.executor is not dummy_executor,
- "wsgi.multiprocess": True,
- "wsgi.run_once": False,
- }
- if "Content-Type" in request.headers:
- environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
- if "Content-Length" in request.headers:
- environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
- for key, value in request.headers.items():
- environ["HTTP_" + key.replace("-", "_").upper()] = value
- return environ
- def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
- if status_code < 400:
- log_method = access_log.info
- elif status_code < 500:
- log_method = access_log.warning
- else:
- log_method = access_log.error
- request_time = 1000.0 * request.request_time()
- assert request.method is not None
- assert request.uri is not None
- summary = (
- request.method # type: ignore[operator]
- + " "
- + request.uri
- + " ("
- + request.remote_ip
- + ")"
- )
- log_method("%d %s %.2fms", status_code, summary, request_time)
- HTTPRequest = httputil.HTTPServerRequest
|