Как обрабатывать 413: запрос слишком большой сущности на сервере фляги Python

Я использую Flask-uploads для загрузки файлов на мой сервер Flask. Максимально допустимый размер устанавливается с помощью flaskext.uploads.patch_request_class(app, 16 * 1024 * 1024).

Мое клиентское приложение (модульный тест) использует запросы для публикации слишком большого файла.

Я вижу, что мой сервер возвращает HTTP-ответ со статусом 413: Request Entity Too Large. Но клиент вызывает исключение в коде запросов.

ConnectionError: HTTPConnectionPool(host='api.example.se', port=80): Max retries exceeded with url: /images (Caused by <class 'socket.error'>: [Errno 32] Broken pipe)

Я предполагаю, что сервер отключает принимающий сокет и отправляет ответ обратно клиенту. Но когда клиент получает сломанный сокет отправки, он вызывает исключение и пропускает ответ.

Вопросов:

  • Верно ли мое предположение о Flask-Uploads и запросах?
  • Правильно ли Flask-Uploads и запрос обрабатывают ошибку 413?
  • Должен ли я ожидать, что мой клиентский код вернет некоторый HTML-код, когда сообщение будет слишком большим?

Обновить

Вот простой пример, воспроизводящий мою проблему.

server.py

from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024

@app.route('/post', methods=('POST',))
def view_post():
    return request.data

app.run(debug=True)

client.py

from tempfile import NamedTemporaryFile
import requests

def post(size):
    print "Post with size %s" % size,
    f = NamedTemporaryFile(delete=False, suffix=".jpg")
    for i in range(0, size):
        f.write("CoDe")
    f.close()

    # Post
    files = {'file': ("tempfile.jpg", open(f.name, 'rb'))}
    r = requests.post("http://127.0.0.1:5000/post", files=files)
    print "gives status code = %s" % r.status_code

post(16)
post(40845)
post(40846)

результат от клиента

Post with size 16 gives status code = 200
Post with size 40845 gives status code = 413
Post with size 40846
Traceback (most recent call last):
  File "client.py", line 18, in <module>
    post(40846)
  File "client.py", line 13, in post
    r = requests.post("http://127.0.0.1:5000/post", files=files)
  File "/opt/python_env/renter/lib/python2.7/site-packages/requests/api.py", line 88, in post
    return request('post', url, data=data, **kwargs)
  File "/opt/python_env/renter/lib/python2.7/site-packages/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File "/opt/python_env/renter/lib/python2.7/site-packages/requests/sessions.py", line 357, in request
    resp = self.send(prep, **send_kwargs)
  File "/opt/python_env/renter/lib/python2.7/site-packages/requests/sessions.py", line 460, in send
    r = adapter.send(request, **kwargs)
  File "/opt/python_env/renter/lib/python2.7/site-packages/requests/adapters.py", line 354, in send
    raise ConnectionError(e)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=5000): Max retries exceeded with url: /post (Caused by <class 'socket.error'>: [Errno 32] Broken pipe)

мои версии

$ pip freeze
Flask==0.10.1
Flask-Mail==0.9.0
Flask-SQLAlchemy==1.0
Flask-Uploads==0.1.3
Jinja2==2.7.1
MarkupSafe==0.18
MySQL-python==1.2.4
Pillow==2.1.0
SQLAlchemy==0.8.2
Werkzeug==0.9.4
blinker==1.3
itsdangerous==0.23
passlib==1.6.1
python-dateutil==2.1
requests==2.0.0
simplejson==3.3.0
six==1.4.1
virtualenv==1.10.1
voluptuous==0.8.1
wsgiref==0.1.2

person Arlukin    schedule 18.10.2013    source источник


Ответы (3)


Flask закрывает соединение, вы можете установить обработчик ошибок для ошибки 413:

@app.errorhandler(413)
def request_entity_too_large(error):
    return 'File Too Large', 413

Теперь клиент должен получить ошибку 413, обратите внимание, что я не тестировал этот код.

Обновление:

Я попытался воссоздать ошибку 413, но не получил исключения ConnectionError.

Вот краткий пример:

from flask import Flask, request

app = Flask(__name__)

app.config['MAX_CONTENT_LENGTH'] = 1024


@app.route('/post', methods=('POST',))
def view_post():
    return request.data


app.run(debug=True)

После запуска файла я использовал терминал для тестирования запросов и отправки больших данных:

>>> import requests
>>> r = requests.post('http://127.0.0.1:5000/post', data={'foo': 'a'})
>>> r
<Response [200]>
>>> r = requests.post('http://127.0.0.1:5000/post', data={'foo': 'a'*10000})
>>> r
<Response [413]>
>>> r.status_code
413
>>> r.content
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>413 Request Entity Too Large</title
>\n<h1>Request Entity Too Large</h1>\n<p>The data value transmitted exceeds the capacity limit.</p>\n'

Как видите, мы получили ответ с ошибкой 413 фляги, и запросы не вызывали исключения.

Кстати использую:

  • Колба: 0.10.1
  • Запросы: 2.0.0
person Pierre    schedule 19.10.2013
comment
Я обновил свой вопрос примером, основанным на ваших примерах. Разница в моем примере в том, что я использую файл вместо данных. И похоже, что я нашел какой-то размер буфера или какой он есть. - person Arlukin; 21.10.2013
comment
@Arlukin Я пробовал ваш код, и он работает нормально, и я получаю ошибку 413 и никаких исключений даже с большими файлами, я использую те же версии, что и ваша, и я использую Python 2.7.5, в любом случае я ' Постараюсь разобраться и обновить свой ответ. - person Pierre; 21.10.2013

RFC 2616, спецификация для HTTP 1.1, гласит:

10.4.14 413 Слишком большой объект запроса

Сервер отказывается обрабатывать запрос, потому что объект запроса
больше, чем сервер хочет или может обработать.
сервер МОЖЕТ закрыть соединение, чтобы предотвратить продолжение запроса клиентом.

Если условие является временным, серверу СЛЕДУЕТ включить поле заголовка Retry-
After, чтобы указать, что оно временное и через какое время клиент МОЖЕТ повторить попытку.

Вот что здесь происходит: flask закрывает соединение, чтобы клиент не мог продолжить загрузку, что приводит к ошибке Broken pipe.

person Christian Ternus    schedule 18.10.2013
comment
Отлично, чем приложение действует правильно. Всегда забывайте о RFC: s. Теперь я также проверил с помощью curl, и сервер Flask работает правильно. Отбрасывает принимающую трубу, но отправляет обратно сообщение об ошибке. Так что это запросы, которые работают не так, как ожидалось. - person Arlukin; 20.10.2013

На основе ответов на эту проблему github (https://github.com/benoitc/gunicorn/issues/1733#issuecomment-377000612)

@app.before_request
def handle_chunking():
    """
    Sets the "wsgi.input_terminated" environment flag, thus enabling
    Werkzeug to pass chunked requests as streams.  The gunicorn server
    should set this, but it's not yet been implemented.
    """

    transfer_encoding = request.headers.get("Transfer-Encoding", None)
    if transfer_encoding == u"chunked":
        request.environ["wsgi.input_terminated"] = True
person Milad Fadavvi    schedule 16.12.2019