Search code examples
javascripttornado

Tornado stream_request_body: how to write custom error msg to document element?


I have a tornado app using stream_request_body for uploading a file to server. File selection is a HTML form where JS onsubmit function is used to execute the upload handler. The JS function is async with await fetch. In case the user chooses a file above max allowed size then I use self.set_status(400) in def prepare(self). I would in this case also like to send/write a text string (self.write('File too big')?) that should be displayed in an element in the document as information to the user, how do I do this?

With my current JS script I get an error in the browser console:

Promise { <state>: "pending" }
TypeError: Response.json: Body has already been consumed.

Another issue I have with the setup of the tornado server is that eventhough I have a return in the def prepare(self) function when the file is larger than max allowed, then def data_received and def post are executed (the file is actually uploaded to server), why is that?

Any help/hints appreciated. I am new to tornado and JS, so sorry if the questions are very basic.

Using tornado ver 6.1, python 3.9

application.py

from tornado import version as tornado_version
from tornado.ioloop import IOLoop
import tornado.web
import uuid
import os
import json


MB = 1024 * 1024
GB = 1024 * MB
MAX_STREAMED_SIZE = 1024 #20 * GB

@tornado.web.stream_request_body
class UploadHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.bytes_read = 0
        self.loaded = 0
        self.data = b''

    def prepare(self):
        self.content_len = int(self.request.headers.get('Content-Length'))

        if self.content_len > MAX_STREAMED_SIZE:
            txt = "Too big file"
            print(txt)
            self.set_status(400)
            # how do I pass this txt to an document element?
            self.write(json.dumps({'error': txt}))

            # eventhough I have a return here execution is continued
            # in data_received() and post() functions 
            # Why is that?
            return 

    def data_received(self, chunk):
        self.bytes_read += len(chunk)
        self.data += chunk

    def post(self):
        value = self.data
        fname = str(uuid.uuid4())
        with open(fname, 'wb') as f:
            f.write(value)

        data = {'filename': fname}

        print(json.dumps(data))
        self.write(json.dumps(data))
        

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('index.html')

def main():
    handlers = [(r'/', IndexHandler), (r'/upload', UploadHandler)]

    settings = dict(debug=True, template_path=os.path.dirname(__file__))

    app = tornado.web.Application(handlers, **settings)
    print(app)
    app.listen(9999, address='localhost')

    IOLoop().current().start()


if __name__ == '__main__':
    print('Listening on localhost:9999')
    print('Tornado ver:', tornado_version)
    main()

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Upload something!</title>
    </head>
    <body>
    <h1>Upload</h1>
    <form id="uploadForm">
        <input type="file" name="file" id="file" />
        <br />
        <input type="submit" value="Upload">
    </form>
    <p><span id='display'></span></p>
    <script>
    uploadForm.onsubmit = async (e) => {
        e.preventDefault();
        var fileInput = document.getElementById('file');
        var fileAttr = fileInput.files[0];
        console.log(fileAttr);
        var filename = fileInput.files[0].name;
        console.log(filename);

        document.getElementById('display').innerHTML = 
            'Uploading ' + document.getElementById("file").value;

        let formData = new FormData(document.getElementById('uploadForm'));
        try {
            let response = await fetch(`${window.origin}/upload`, {
                method: "POST",
                body: formData,
            });
            if (!response.ok) {
                console.log('error')
                console.log(response.json());
                // how do I update document.getElementById('display').innerHTML
                // with tornado self.write when error response?
            }
            let result = await response.json();
            console.log(result);
            document.getElementById('display').innerHTML = 'Finished';
        } catch(exception) {
            console.log(exception);
        } 
    };
    </script>
    </body>
</html>

Solution

  • In prepare it is not enough to return, you need to raise an exception to stop the processing.

    So you have two options:

    1. use provided features: overwrite write_error on your RequestHandler to create custom error responses, then raise tornado.web.HTTPError(400)[1] in prepare after your print

    2. do everything yourself: use self.set_status to set an error status code, self.write, to write out whatever you need on the spot, then raise tornado.web.Finish to short circuit the processing of the request.

    With your code as it is, you basically only need to replace the return in prepare with a raise tornado.web.Finish(). Obviously if you were going to do this in multiple places it makes sense to use #1, but if you only have the script you have now, #2 will do just fine.