Search code examples
flaskgunicorn

Request Entity Too Large for 30-Second Audio File (WebM)


I'm building a Flask web application where users can record and submit 30 seconds of audio (WebM format) through a form. However, when I try to submit a 30-second recording (~650 KB), I get a 413 Request Entity Too Large error.

I've tried the following:

Set MAX_CONTENT_LENGTH in Flask:

app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB limit

Running Flask with Waitress:

from waitress import serveserve
(app, host="127.0.0.1", port=5000, max_request_body_size=100 * 1024 * 1024)

Testing Gunicorn with gunicorn_config.py:

limit_request_field_size = 0
limit_request_line = 0
timeout = 300
worker_connections = 1000

Despite these changes, submitting larger requests (30 seconds of audio) still triggers the 413 error.

Here’s what I’ve ruled out:

  • Small audio files (~100 KB) work fine.
  • MacOS firewall is disabled.
  • I'm not using Nginx.

Code Snippets

Frontend (JavaScript) – Recording and Submitting Audio

function processRecording() {
    let audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
    let reader = new FileReader();
    reader.readAsDataURL(audioBlob);

    reader.onloadend = function () {
        audioDataInput.value = reader.result;
        document.querySelector('form').submit();
    };
}

Backend (Flask Route for Submitting Audio)

@app.route('/speaking_task_submit', methods=['POST'])
def speaking_task_submit():
    print(f"Flask MAX_CONTENT_LENGTH: {app.config.get('MAX_CONTENT_LENGTH')} bytes")
    print(f"Received Content-Length: {request.content_length} bytes")

    if request.content_length and request.content_length > app.config.get('MAX_CONTENT_LENGTH'):
        return jsonify({"error": "Request size exceeded!"}), 413

    audio_data = request.form['audio_data']
    if not audio_data or "," not in audio_data:
        return jsonify({"error": "Invalid or missing audio data"}), 400

    # Save audio as .webm
    audio_content = audio_data.split(",")[1]
    audio_file_path = f"uploads/candidate_audio.webm"
    with open(audio_file_path, "wb") as audio_file:
        audio_file.write(base64.b64decode(audio_content))

    return jsonify({"message": "Audio received successfully"})

Environment Details:

  • MacOS Monterey
  • Python 3.10
  • Flask 3.1.0
  • Waitress 3.0.2
  • Gunicorn 23.0.0

What I’ve Tried:

  • Running Flask with Waitress and setting max_request_body_size=100 * 1024 * 1024.
  • Running Flask with Gunicorn and setting limit_request_field_size = 0.
  • Disabled any firewall or proxy limits on MacOS.

Question:

  • What could still be limiting the request size?
  • Is there another hidden limit in Waitress, Gunicorn, or Flask that I’m missing?
  • Should I configure any additional middleware settings in Flask?

Any help is greatly appreciated!


Solution

  • The error message results from the way you upload the data. I use the following code to upload the blob directly in a form as a file and have no problems.
    The method used is more efficient for larger audio files, as FormData objects have no size restrictions and no decoding is required. The data is sent in binary format and is therefore smaller than base64 encoded data.

    from flask import (
        Flask,
        abort,
        current_app, 
        render_template,
        redirect,
        request,
        stream_with_context,
        url_for
    
    )
    from collections import namedtuple
    from glob import glob
    from mimetypes import add_type, guess_extension, guess_type
    from werkzeug.utils import secure_filename
    import os
    
    
    add_type('audio/aac', '.m4a', strict=True)
    
    Record = namedtuple('Record', ('filename', 'created_at'))
    
    app = Flask(__name__)
    app.config.from_mapping(
        MAX_CONTENT_LENGTH=100*1024*1024, 
        UPLOAD_FOLDER='uploads', 
    )
    
    os.makedirs(
        os.path.join(
            app.instance_path,
            app.config.get('UPLOAD_FOLDER', 'uploads')
        ), 
        exist_ok=True
    )
    
    @app.route('/')
    def audio_index():
        patterns = [
            '*.m4a', 
            '*.wav', 
            '*.weba' 
        ]
        path = os.path.join(
            current_app.instance_path,
            current_app.config.get('UPLOAD_FOLDER', 'uploads')
        )
        records = [
            Record(fn[len(path)+1:], os.path.getctime(fn)) \
             for ptrn in patterns for fn in glob(os.path.join(path, ptrn))
        ]
        return render_template('index.html', records=records)
    
    @app.post('/audio-upload')
    def audio_upload():
        if 'audio_file' in request.files:
            file = request.files['audio_file']
            extname = guess_extension(file.mimetype)
            if not extname:
                abort(400)
    
            # Check for allowed file extensions.
    
            i = 1
            while True:
                dst = os.path.join(
                    current_app.instance_path,
                    current_app.config.get('UPLOAD_FOLDER', 'uploads'),
                    secure_filename(f'audio_record_{i}{extname}'))
                if not os.path.exists(dst): break
                i += 1
    
            file.save(dst)
    
        return redirect(url_for('audio_index'))
    
    @app.route('/audio/stream/<path:filename>')
    def audio_download(filename):
        @stream_with_context
        def generator(src):
            CHUNK_SIZE = 8*1024
            with open(src, 'rb') as fp:
                while True:
                    data = fp.read(CHUNK_SIZE)
                    if not data: break
                    yield data
    
        src = os.path.join(
            current_app.instance_path,
            current_app.config.get('UPLOAD_FOLDER', 'uploads'),
            filename
        )
    
        if not os.path.exists(src):
            return abort(404)
    
        mime,_ = guess_type(src)
        return app.response_class(generator(src), mimetype=mime)
    
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Audio Index</title>
      </head>
      <body>
          <div class="rec-container">
            <div class="rec-column rec-column-1">
              <button class="rec-btn" id="toggle-rec-btn">Record</button>
            </div>
            <div class="rec-column rec-column-2">
              {% for record in records|sort(attribute='created_at', reverse=True) -%}
              <div class="rec-item">
                <div class="content">
                  <span class="rec-title">{{ record.filename }}</span>
                  <audio
                    controls
                    src="{{ url_for('audio_download', filename=record.filename) }}"
                  ></audio>
                </div>
              </div>
              {% endfor -%}
            </div>
        </div>
    
        <script type="text/javascript">
        (function(uploadURL) {
          const startButton = document.getElementById('toggle-rec-btn');
          startButton.addEventListener('click', function() {
            if (!navigator.mediaDevices) {
              console.error('MediaDevices not supported.')
              return;
            }
    
            const mimeType = 'audio/webm;codecs=opus';
            if (!MediaRecorder.isTypeSupported(mimeType)) {
              console.error('Unsupported media type.');
              return;
            }
    
            const constraints = { audio: true };
            navigator.mediaDevices.getUserMedia(constraints)
            .then(stream => {
                let chunks = []
                const options = { mimeType: mimeType };
                const recorder = new MediaRecorder(stream, options);
                recorder.ondataavailable = event => {
                    chunks.push(event.data);
                };
                recorder.onstop = event => {
                  console.log('Recording stopped.');
                  const blob = new Blob(chunks, { type: recorder.mimeType }); 
                  chunks = [];
                  startButton.disabled = false;
    
                  const formData = new FormData();
                  formData.append('audio_file', blob);
    
                  fetch(uploadURL, {
                    method: 'POST',
                    cache: 'no-cache',
                    body: formData
                  }).then(resp => {
                    if (!resp.ok) {
                      throw new Error('Something went wrong.');
                    }
                    window.location.reload(true);
                  }).catch(err => {
                    console.error(err);
                  });
                };
                recorder.onstart = event => {
                  console.log('Recording started.');
                  startButton.disabled = true;
                  setTimeout(function() { recorder.stop(); }, 30000);
                };
                recorder.start();
            })
            .catch(err => console.error(err));
          });
        })({{ url_for('audio_upload') | tojson }});
        </script>
      </body>
    </html>