tornadoweb / tornado

Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.
http://www.tornadoweb.org/
Apache License 2.0
21.77k stars 5.51k forks source link

Add RequestParsingMiddleware for Simplified Request Body Parsing (Fixes #3369) #3427

Closed Nirab123456 closed 1 month ago

Nirab123456 commented 1 month ago

Description

This pull request introduces the RequestParsingMiddleware, a middleware component designed to simplify the process of parsing request bodies in Tornado applications. This implementation addresses the concerns raised in issue #3369 regarding the limitations of the tornado.httputil.HTTPServerRequest class, particularly the need for manual parsing of request bodies.

Key Features:

Example Use Case

The following example demonstrates how to use the RequestParsingMiddleware in a Tornado application to effectively handle both form submissions and file uploads:

# app.py
import os
import tornado.ioloop
import tornado.web
from tornado.webmiddleware import RequestParsingMiddleware

class MainHandler(tornado.web.RequestHandler):
    async def prepare(self):
        self.parsed_body = None
        await self._apply_middlewares()  # Await middleware processing

    async def _apply_middlewares(self):
        middlewares = [RequestParsingMiddleware()]
        for middleware in middlewares:
            await middleware.process_request(self)  # Await middleware

    async def get(self):
        # Render the HTML form for user input
        self.write("""
            <html>
                <body>
                    <h1>Submit Your Information</h1>
                    <form action="/parse" method="post" enctype="multipart/form-data">
                        <label for="name">Name:</label><br>
                        <input type="text" id="name" name="name"><br><br>
                        <label for="file">Upload File:</label><br>
                        <input type="file" id="file" name="file"><br><br>
                        <input type="submit" value="Submit">
                    </form>
                </body>
            </html>
        """)

    async def post(self):
        # Access parsed body data
        name = self.parsed_body["arguments"].get("name", [""])[0]  # Getting the name field
        files = self.parsed_body["files"].get("file", [])  # Getting uploaded files

        # Prepare response HTML
        response_html = "<h1>Submitted Information</h1>"
        response_html += f"<p>Name: {name}</p>"

        if files:
            response_html += "<h2>Uploaded Files:</h2><ul>"
            for file_info in files:
                response_html += f"<li>{file_info['filename']}</li>"
                # Save the file to the server
                await self.save_file(file_info)  # Await the file saving process
            response_html += "</ul>"

        self.write(response_html)

    async def save_file(self, file_info):
        # Define the upload directory
        upload_dir = 'uploads'
        if not os.path.exists(upload_dir):
            os.makedirs(upload_dir)  # Create the directory if it doesn't exist

        # Save the uploaded file asynchronously
        file_path = os.path.join(upload_dir, file_info['filename'])
        with open(file_path, 'wb') as f:
            f.write(file_info['body'])  # Write the raw file body to disk

class HomeHandler(tornado.web.RequestHandler):
    async def get(self):
        self.redirect("/parse")  # Redirect to the /parse route

def make_app():
    return tornado.web.Application([
        (r"/", HomeHandler),  # Add root route
        (r"/parse", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("Server is running on http://localhost:8888")
    tornado.ioloop.IOLoop.current().start()

Conclusion

The RequestParsingMiddleware provides a robust and user-friendly solution for parsing request bodies in Tornado applications. By integrating this middleware, developers can streamline their request handling processes, improve overall application performance, and enhance reliability. This pull request aims to elevate the Tornado framework's usability, especially for new developers navigating the complexities of request parsing.

bdarnell commented 1 month ago
  1. This "middleware" interface is not integrated at all with the rest of tornado (I'm guessing because it was written by a chatbot). The documentation suggests that the user define their own _apply_middleware method and call it from prepare but this is an unnecessary abstraction compared to just calling self.parse_body().
  2. Treating JSON and HTML forms similarly is initially appealing, but has inconveniences in practice (most significantly that parse_body_arguments must always return values wrapped in lists because the HTML form encoding is ambiguous). I'm opposed to doing something like this in Tornado; I'd rather keep the existing "arguments" functions for HTML forms and when/if we support JSON do it through a separate interface.

The recommended solution for json arguments is therefore something like

    def prepare(self):
        self.parsed_body = json.loads(self.request.body)

Any "middleware" solution that wants to provide alternative argument handling needs to improve on this situation, and it's hard (but not impossible!) to beat a one-liner that can be put in a base class. If you want to try and tackle this problem I suggest starting with hashing out the design in an issue first, before moving on to implementation in a PR.