Fixing 'Request Too Large (max 20mb)': A Deep BreakDown for Senior Developers

14 min read 2,820 words PookieTech Team
Fixing 'Request Too Large (max 20mb)': A Deep BreakDown for Senior Developers

Fixing 'Request Too Large (max 20mb)': A Deep BreakDown for Senior Developers

That "request too large (max 20mb)" error, often followed by a prompt to "double press esc to go back and try with a smaller file," is a common friction point in web applications. It's frustrating for users and, more importantly, a signal that our systems aren't configured to handle expected payloads. This isn't just about bumping a number; it often points to deeper architectural considerations around file uploads, service boundaries, and user experience. We've all encountered it: a user tries to upload a seemingly innocuous file – maybe a high-resolution image, a detailed CSV, or a video snippet – and hits an arbitrary wall. The 20MB limit is just an example; it could be 1MB, 50MB, or 100MB. The underlying issue remains consistent: a request payload exceeding a configured maximum. Diagnosing and resolving this requires looking across the entire request path, from the client's browser to the application server and any proxies in between.

The Core Problem: Where "Request Too Large" Hits

The "request too large" error indicates a hard limit on the size of an HTTP request body. This limit can be enforced at multiple layers:

  1. Client-Side (Browser/Application): While less common for a hard "too large" error message originating from the server, client-side JavaScript can impose limits before the request even leaves the browser. This usually results in a more user-friendly error.
  2. Web Server/Application Framework: Your application server (e.g., Node.js Express, Python Flask/Django, Java Spring Boot) often has its own default or configurable limits on the size of incoming request bodies to prevent resource exhaustion.
  3. Reverse Proxy/Load Balancer: Services like Nginx, Apache HTTPD, AWS Application Load Balancers (ALB), or Google Cloud Load Balancers sit in front of your application servers. They buffer incoming requests and enforce their own maximum body size limits before forwarding traffic. This is a very common place for this error to originate.
  4. API Gateway: If you're using an API Gateway (e.g., Kong, Apigee, AWS API Gateway), these also have configurable payload size limits.

The error message "double press esc to go back" suggests a client-side UI reacting to a server-side 413 Payload Too Large HTTP status code. Our job is to trace where that 413 is being generated and adjust the limit, or, more robustly, refactor the upload mechanism.

Diagnosing the Bottleneck: Client, Server, or Proxy?

Before blindly increasing limits, we need to pinpoint the exact component enforcing the constraint.

Browser-Side Investigation

Start with your browser's developer tools. Open the Network tab, attempt the upload, and observe the failed request.

  1. Status Code: A 413 Payload Too Large status code is the definitive indicator.
  2. Response Headers/Body: Examine the response. Does it come from your application server, a proxy, or a CDN? The Server header can often provide clues (e.g., Server: Nginx, Server: Kestrel, Server: Apache). The response body might contain custom error messages or a generic proxy error page.
  3. Timing: Does the request fail almost instantly, or after some data has been uploaded? Instant failure often points to a proxy or API Gateway. A failure after a partial upload might indicate a server-side limit or a network timeout.

A quick curl test can also help bypass the browser for initial diagnosis.


# Create a dummy file larger than 20MB
dd if=/dev/zero of=large_file.bin bs=1M count=25

# Attempt a POST request with the large file
curl -v -X POST -H "Content-Type: application/octet-stream" \
     --data-binary "@large_file.bin" \
     https://your-api-endpoint.com/upload

The -v flag will show verbose output, including request and response headers, helping you see the 413 status and potentially the server that sent it.

Server-Side Logs

Check your application server logs (e.g., PM2 logs for Node.js, Gunicorn/uWSGI logs for Python, Tomcat/Undertow logs for Java). If the request reaches your application, you might see errors related to body parsing, input stream limits, or similar. If you don't see any logs for the failed request, it likely didn't even reach your application, pointing to a proxy or gateway.

Proxy/Gateway Checks

Examine the logs of any reverse proxies, load balancers, or API Gateways in front of your application. * Nginx/Apache: Look in /var/log/nginx/error.log or /var/log/apache2/error.log. You'll often see entries like "client intended to send too large body." * Cloud Load Balancers (AWS ALB, GCP Load Balancer): Check access logs and error logs. These typically have default limits that might need increasing. For AWS ALB, the "Idle timeout" is often the relevant setting for long-running requests, but explicit body size limits can also apply, though less common as a direct configuration for ALBs themselves (they typically pass through large bodies up to their timeout, leaving the backend to enforce). However, if an API Gateway (like AWS API Gateway) is in front of the ALB, it will have stricter limits.

Client-Side Strategies: Proactive Handling

While increasing server limits is necessary, proactively handling large files on the client side improves UX and reduces server load.

Pre-Validation and User Feedback

Preventing the upload of oversized files *before* they're sent saves bandwidth and provides immediate feedback.


<input type="file" id="fileInput" />
<button onclick="uploadFile()">Upload</button>
<p id="errorMessage" style="color: red;"></p>

<script>
    const MAX_FILE_SIZE_MB = 20; // Match your server's initial limit
    const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;

    function uploadFile() {
        const fileInput = document.getElementById('fileInput');
        const errorMessage = document.getElementById('errorMessage');
        errorMessage.textContent = '';

        if (fileInput.files.length === 0) {
            errorMessage.textContent = 'Please select a file.';
            return;
        }

        const file = fileInput.files[0];

        if (file.size > MAX_FILE_SIZE_BYTES) {
            errorMessage.textContent = `File is too large. Max allowed: ${MAX_FILE_SIZE_MB}MB.`;
            // Prevent actual upload, provide user with guidance
            return;
        }

        // Proceed with actual upload (e.g., using FormData and fetch API)
        const formData = new FormData();
        formData.append('file', file);

        fetch('/upload', {
            method: 'POST',
            body: formData
        })
        .then(response => {
            if (!response.ok) {
                // Handle server-side errors, e.g., 413 Payload Too Large
                if (response.status === 413) {
                    errorMessage.textContent = `Server rejected file: too large. Max allowed: ${MAX_FILE_SIZE_MB}MB.`;
                } else {
                    errorMessage.textContent = `Upload failed: ${response.statusText}`;
                }
                throw new Error('Upload failed');
            }
            return response.json();
        })
        .then(data => {
            console.log('Upload successful:', data);
            errorMessage.textContent = 'Upload successful!';
        })
        .catch(error => {
            console.error('Error during upload:', error);
            if (!errorMessage.textContent) { // Only update if not already set by 413
                errorMessage.textContent = 'An unexpected error occurred during upload.';
            }
        });
    }
</script>

This client-side check is a first line of defense, but it's easily bypassed. Server-side validation is still mandatory.

Chunked Uploads and Resumable Transfers

For genuinely large files (hundreds of MBs to several GBs), chunked uploads are the robust solution. Instead of sending one massive HTTP request, the file is split into smaller, manageable chunks. Each chunk is sent as a separate request. This approach offers:

  • Resumability: If a network error occurs, only the failed chunk needs to be re-sent, not the entire file.
  • Reduced Memory Usage: Both client and server process smaller data segments.
  • Bypass Request Size Limits: Each chunk is well within typical limits.

Protocols like TUS (The Upload STandard) formalize this, providing resumable file uploads over HTTP. Libraries like Uppy integrate TUS and other chunking mechanisms seamlessly.


// Example using Uppy with the TUS plugin (conceptual, requires Uppy setup)
// This is a high-level overview; a full implementation involves Uppy installation
// and server-side TUS endpoint setup.

import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import Dashboard from '@uppy/dashboard';

const uppy = new Uppy({
    debug: true,
    autoProceed: false,
    restrictions: {
        maxFileSize: 500 * 1024 * 1024, // Example: 500MB client-side limit for Uppy
        maxNumberOfFiles: 1,
        minNumberOfFiles: 1,
        allowedFileTypes: ['image/*', 'video/*']
    }
})
.use(Dashboard, {
    inline: true,
    target: '#dashboard-container'
})
.use(Tus, {
    endpoint: 'https://your-tus-server.com/files/', // Your TUS server endpoint
    chunkSize: 5 * 1024 * 1024, // 5MB chunks
    retryDelays: [0, 1000, 3000, 5000] // Retry after 0, 1, 3, 5 seconds
});

uppy.on('complete', result => {
    console.log('Upload successful! Files:', result.successful);
});

uppy.on('file-added', (file) => {
    console.log('File added:', file.name);
});

uppy.on('upload-error', (file, error) => {
    console.error('Upload error:', error);
});

// To trigger upload programmatically:
// document.getElementById('uploadButton').addEventListener('click', () => uppy.upload());

The server-side for TUS needs to implement the TUS protocol, managing chunk assembly and storage.

Client-Side Compression (with caveats)

While possible, client-side compression (e.g., using JavaScript libraries like `pako.js` for GZIP) is generally not recommended for generic file uploads unless the file type is inherently uncompressed (e.g., raw text, CSVs) and you control both client and server. For images and videos, they are often already compressed, and re-compressing them can lead to quality loss or negligible size reduction. It also adds CPU overhead to the client.

Server-Side Configuration: Increasing Limits

If you've determined the limit is enforced by your application server, here's how to adjust it for common stacks.

Node.js (Express)

Express uses middleware like body-parser or multer for handling request bodies, including file uploads.


// server.js
const express = require('express');
const bodyParser = require('body-parser');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = 3000;

// Ensure upload directory exists
const uploadDir = 'uploads/';
if (!fs.existsSync(uploadDir)){
    fs.mkdirSync(uploadDir);
}

// 1. For JSON/URL-encoded bodies: Increase body-parser limit
// Default is '100kb'. Setting to '50mb' to handle larger JSON payloads.
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));

// 2. For file uploads (multipart/form-data): Use Multer
// Multer's limits are configured separately.
// 'limits' option in Multer allows setting file size limits.
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, uploadDir);
    },
    filename: function (req, file, cb) {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
    }
});

const upload = multer({
    storage: storage,
    limits: {
        fileSize: 50 * 1024 * 1024 // 50 MB (in bytes)
    },
    fileFilter: (req, file, cb) => {
        // Optional: file type validation
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif|pdf|csv|xlsx|docx)$/)) {
            return cb(new Error('Only specific file types are allowed!'), false);
        }
        cb(null, true);
    }
});

app.post('/upload-single', upload.single('myFile'), (req, res) => {
    if (!req.file) {
        return res.status(400).send('No file uploaded.');
    }
    console.log(`File uploaded: ${req.file.filename} (${req.file.size} bytes)`);
    res.status(200).json({ message: 'File uploaded successfully!', filename: req.file.filename });
});

app.post('/upload-multiple', upload.array('myFiles', 5), (req, res) => {
    if (!req.files || req.files.length === 0) {
        return res.status(400).send('No files uploaded.');
    }
    const filenames = req.files.map(file => file.filename);
    console.log(`Files uploaded: ${filenames.join(', ')}`);
    res.status(200).json({ message: 'Files uploaded successfully!', filenames: filenames });
});

// Generic error handler for Multer limits
app.use((err, req, res, next) => {
    if (err instanceof multer.MulterError) {
        if (err.code === 'LIMIT_FILE_SIZE') {
            return res.status(413).send(`File too large. Max allowed: ${upload.limits.fileSize / (1024 * 1024)}MB`);
        }
        // Handle other Multer errors
        return res.status(400).send(`Multer error: ${err.message}`);
    }
    if (err) {
        return res.status(500).send(`Server error: ${err.message}`);
    }
    next();
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

For Node.js, ensure both `body-parser` (for non-file POST bodies) and `multer` (for `multipart/form-data`) are configured with appropriate limits.

Python (Flask/Django)

Flask (using Werkzeug) Flask applications use Werkzeug under the hood, which has a `MAX_CONTENT_LENGTH` configuration.


# app.py
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

# Set max content length for Flask (Werkzeug)
# 50 MB in bytes
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024

UPLOAD_FOLDER = 'uploads'
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part in the request'}), 400
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    if file:
        filename = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
        file.save(filename)
        return jsonify({'message': f'File {file.filename} uploaded successfully!'}), 200
    return jsonify({'error': 'Something went wrong'}), 500

# Error handler for file size limit
@app.errorhandler(413)
def request_entity_too_large(error):
    return jsonify({'error': 'File too large. Max allowed: 50MB'}), 413

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Django Django has `DATA_UPLOAD_MAX_MEMORY_SIZE` and `FILE_UPLOAD_MAX_MEMORY_SIZE` settings in `settings.py`. These control how much data is buffered in memory before being written to disk.


# settings.py
# 50 MB (in bytes)
DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800 # 50 * 1024 * 1024
FILE_UPLOAD_MAX_MEMORY_SIZE = 52428800 # 50 * 1024 * 1024

# If you need to handle larger files that are streamed to disk
# (which is the default behavior for files larger than FILE_UPLOAD_MAX_MEMORY_SIZE),
# you might need to increase timeout settings on your web server/proxy.

Django's default file upload handler streams files larger than `FILE_UPLOAD_MAX_MEMORY_SIZE` to disk, effectively bypassing a hard memory-based limit for the entire request body, but you still need to ensure your proxy and web server can handle the sustained connection.

Go (net/http)

Go's standard library `net/http` server also allows setting a maximum request body size.


// main.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"time"
)

const maxUploadSize = 50 << 20 // 50 MB

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// 1. Limit the size of the request body
	r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

	// 2. Parse the multipart form
	// MaxMemory argument specifies how much of the file to store in memory
	// before writing to a temporary file. Set it to a reasonable value,
	// e.g., 10MB, or maxUploadSize if you want to keep smaller files in memory.
	err := r.ParseMultipartForm(10 << 20) // 10 MB
	if err != nil {
		if err.Error() == "http: request body too large" {
			http.Error(w, fmt.Sprintf("File too large. Max allowed: %dMB", maxUploadSize/(1<<20)), http.StatusRequestEntityTooLarge)
			return
		}
		http.Error(w, fmt.Sprintf("Error parsing form: %v", err), http.StatusBadRequest)
		return
	}

	// Get the file from the form
	file, handler, err := r.FormFile("myFile")
	if err != nil {
		http.Error(w, fmt.Sprintf("Error retrieving file from form: %v", err), http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Create a directory for uploads if it doesn't exist
	uploadDir := "uploads"
	if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
		os.Mkdir(uploadDir, 0755)
	}

	// Create the destination file on disk
	dstPath := filepath.Join(uploadDir, handler.Filename)
	dst, err := os.Create(dstPath)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error creating destination file: %v", err), http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	// Copy the uploaded file to the destination
	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, fmt.Sprintf("Error saving file: %v", err), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "File %s uploaded successfully!", handler.Filename)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)

	fmt.Printf("Server starting on port 8080\n")
	server := &http.Server{
		Addr:              ":8080",
		ReadHeaderTimeout: 5 * time.Second, // Prevent slowloris attacks
		ReadTimeout:       10 * time.Second, // Total time to read the request, including body
		WriteTimeout:      10 * time.Second, // Total time to write the response
		IdleTimeout:       30 * time.Second, // Max time for connections to remain idle
	}

	// For large file uploads, ReadTimeout and WriteTimeout might need to be adjusted
	// depending on expected upload speeds and file sizes.
	// However, the http.MaxBytesReader is the primary control for body size.

	if err := server.ListenAndServe(); err != nil {
		fmt.Printf("Server failed: %v\n", err)
	}
}

The `http.MaxBytesReader` is key here for Go.

Java (Spring Boot/Tomcat)

For Spring Boot applications, the embedded Tomcat, Jetty, or Undertow server has its own configurations.


# application.properties (for Spring Boot)

# Max file size for a single file (e.g., in multipart requests)
spring.servlet.multipart.max-file-size=50MB

# Max request size for the entire multipart request (sum of all files + form data)
spring.servlet.multipart.max-request-size=50MB

# Location to store temporary files (defaults to system temp dir)
# spring.servlet.multipart.location=/tmp/uploads

# If running on a standalone Tomcat, configure in server.xml:
# <Connector port="8080" protocol="HTTP/1.1"
#            connectionTimeout="20000"
#            redirectPort="8443"
#            maxPostSize="52428800" /> <!-- 50 MB in bytes -->

For standalone Tomcat, `maxPostSize` in `server.xml` is the crucial setting. For Spring Boot, the `spring.servlet.multipart` properties directly control these limits.

Proxy and Gateway Adjustments: The Front Line

Often, the "request too large" error comes from a proxy sitting in front of your application.

Nginx

Nginx is a very common culprit due to its default `client_max_body_size` directive, which is usually `1m` (1MB).


# /etc/nginx/nginx.conf or a specific site config file in /etc/nginx/sites-available/your-app

http {
    # ... other http settings ...

    # Set client_max_body_size for all servers in this http block (global)
    # client_max_body_size 50m; # 50 MB

    server {
        listen 80;
        server_name your-domain.com;

        # Set client_max_body_size for this specific server block
        # This overrides the http block setting if present
        client_max_body_size 50m; # 50 MB

        location / {
            # ... proxy_pass or root directive ...
            proxy_pass http://your_upstream_app_server; # e.g., http://localhost:3000;

            # Important: Ensure Nginx passes through proper headers for large files
            proxy_read_timeout 300s; # Increase timeout for slow uploads (e.g., 5 minutes)
            proxy_send_timeout 300s;
            proxy_connect_timeout 75s;