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>
In prepare
it is not enough to return, you need to raise an exception to stop the processing.
So you have two options:
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
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.