Main Ecosystems
HomePython CorePandas ReferenceNumPy ScientificFastAPI & PydanticDjango Enterprise
More Ecosystems
Environment & SetupRequests & HTTPAsyncIO ConcurrencyObject-Oriented OOPPyTorch Deep LearningScikit-Learn MLFlask FrameworkWeb ScrapingDatabase & ORMDevOps & Docker

FastAPI: Fixing 422 Unprocessable Entity on React Image Uploads

Verified FixPython 3.10+FastAPI 0.110+Silo: fastapi

Quick Fix / Solution Rapide

This error occurs when Python receives file upload requests with missing or corrupted multipart boundaries caused by manual Content-Type headers in React or Axios. Let the browser set the multipart boundary automatically.

Root Cause Analysis

This error occurs when Python backend endpoints expecting multipart file uploads receive HTTP requests with incorrect Content-Type headers, missing MIME boundaries, or mismatched field keys from frontend applications.

Root Cause 1: Manually Setting Content-Type in React/Axios

When developers write file upload logic in React using Axios or fetch, they often manually set headers: {'Content-Type': 'multipart/form-data'}. Manually specifying this header overrides the browser's native multipart boundary calculation. Without the boundary parameter (such as boundary=----WebKitFormBoundary...), FastAPI and Starlette cannot parse where individual form fields and binary chunks begin or end, resulting in an immediate 422 Unprocessable Entity or 400 Bad Request error.

Root Cause 2: Field Key Name Mismatch

In FastAPI, a file parameter defined as file: UploadFile = File(...) expects the incoming form field key in the FormData object to be named exactly "file". If the React frontend appends the file under a different key name (for example, formData.append('image', selectedFile)), FastAPI fails to find the required "file" field in the request payload and raises a 422 validation error.

Root Cause 3: Sending Files via JSON.stringify

Frontend beginners sometimes place a JavaScript File object inside a plain JSON dictionary and serialize it with JSON.stringify({ file: selectedFile }). Serializing a binary File object into JSON produces an empty {} object, causing FastAPI to reject the payload.

Root Cause 4: Missing File Content-Type Validation

FastAPI endpoints that accept files should validate the MIME type (e.g. file.content_type == 'image/jpeg'). If invalid file extensions or corrupted headers are sent by the client, server-side validation logic should reject them with structured error responses.

Reproduction Code (MCVE)

Example: Bug Reproduction
def validate_multipart_image_request(headers: dict, form_fields: dict):
    content_type = headers.get("content-type", "")
    
    # Check if frontend manually overrode content-type without boundary or sent application/json
    if content_type == "application/json":
        raise ValueError(
            "422 Unprocessable Entity: Expected multipart/form-data with file upload, "
            "but received 'application/json'. Do not JSON.stringify() files in React."
        )
    
    if "multipart/form-data" in content_type and "boundary=" not in content_type:
        raise ValueError(
            "422 Unprocessable Entity: 'multipart/form-data' header missing boundary delimiter. "
            "Do not manually set the Content-Type header in React/Axios."
        )
    
    if "file" not in form_fields:
        raise ValueError(
            "422 Unprocessable Entity: Missing required form field 'file'. "
            f"Found keys: {list(form_fields.keys())}"
        )

# Simulating Axios upload where Content-Type was manually set without boundary parameter
headers_with_bad_boundary = {"content-type": "multipart/form-data"}
validate_multipart_image_request(headers_with_bad_boundary, {"file": b"raw_image_data"})

Solution 1: Remove Manual Headers in React and Match Key Names

In React/Axios, construct a FormData object, append the file using the exact parameter key expected by FastAPI, and let Axios/Fetch set the headers automatically.

Example: Recommended Solution
from fastapi import FastAPI, File, UploadFile, HTTPException, status
import io

app = FastAPI()

@app.post("/upload/image")
async def upload_image(file: UploadFile = File(...)):
    # Validate MIME content type
    allowed_types = ["image/jpeg", "image/png", "image/webp"]
    if file.content_type not in allowed_types:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid file type '{file.content_type}'. Allowed: {allowed_types}"
        )
    
    content = await file.read()
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "size_bytes": len(content)
    }

# Standalone simulation demonstrating correct FormData handling
def process_valid_upload(filename: str, content_type: str, data: bytes):
    assert content_type in ["image/jpeg", "image/png", "image/webp"]
    return {"filename": filename, "bytes_received": len(data), "status": "uploaded"}

result = process_valid_upload("avatar.png", "image/png", b"\x89PNG\r\n\x1a\n...")
print("Upload verified:", result)

Solution 2: Support Optional Files and Multiple Attachments

Handle optional file uploads gracefully using UploadFile | None = None and manage multiple files with list[UploadFile] = File(...).

Example: Alternative Solution
from fastapi import FastAPI, File, UploadFile
from typing import List, Optional

app = FastAPI()

@app.post("/upload/multiple")
async def upload_multiple_images(
    files: List[UploadFile] = File(...),
    thumbnail: Optional[UploadFile] = File(None)
):
    results = []
    for f in files:
        results.append({"filename": f.filename, "type": f.content_type})
    return {"total_files": len(files), "files": results, "has_thumbnail": thumbnail is not None}

# Standalone simulation of multi-file metadata processing
simulated_files = [
    {"name": "photo1.jpg", "size": 1024},
    {"name": "photo2.jpg", "size": 2048}
]
print("Processed", len(simulated_files), "files successfully.")

In modern Axios (v1.0+), passing a FormData object automatically sets Content-Type: multipart/form-data along with the appropriate boundary. Setting headers manually in Axios interceptors or default configurations will break file uploads across the entire frontend application. Check your Axios global instance for default Content-Type: application/json headers that might inadvertently override multipart uploads.

Another common edge case is handling large image uploads. Reading the entire file into memory using await file.read() can exhaust server RAM under high concurrency. For large files (>10MB), stream the chunks directly to disk or cloud storage (e.g., S3/GCS) using while chunk := await file.read(1024 * 1024):.

Contrast 422 Unprocessable Entity with 413 Payload Too Large: 422 indicates that the structure, boundary, or fields are unreadable or missing, whereas 413 indicates that the web server (or Nginx client_max_body_size) rejected the request because the file exceeds maximum allowed upload limits.