I'm trying to upload a file from my React frontend to a Flask-RESTful backend and I'm stuck on a 422 (UNPROCESSABLE ENTITY) error.
My backend logs show the POST request hits the right endpoint, but my reqparse arguments fail validation.
Here is my code:
Frontend (React ResumeUploader.jsx): I'm using axios and FormData to send the file.
//ResumeUploader.jsx
import React, { useState } from 'react';
import axios from 'axios';
import { FiUploadCloud, FiAlertCircle } from 'react-icons/fi';
// This is the correct API endpoint
const UPLOAD_URL = 'http://localhost:5000/api/resume/upload';
const ResumeUploader = ({ setUploadMessage }) => {
const [file, setFile] = useState(null);
const [fileName, setFileName] = useState('No file selected');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
setError('');
setUploadMessage('');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) {
setError('Please select a file to upload.');
return;
}
setLoading(true);
setError('');
setUploadMessage('');
const formData = new FormData();
formData.append('resume', file);
const token = localStorage.getItem('token');
try {
// This is the 100% correct, bug-free request
const res = await axios.post(UPLOAD_URL, formData, {
headers: {
// NO 'Content-Type' header here. This is correct.
'Authorization': `Bearer ${token}`
}
});
setUploadMessage(res.data.message);
setFile(null);
setFileName('No file selected');
} catch (err) {
setError(err.response?.data?.message || err.message || 'Upload failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<label className="flex flex-col items-center justify-center w-full h-32 px-4 transition bg-white border-2 border-gray-300 border-dashed rounded-md appearance-none cursor-pointer hover:border-gray-400 focus:outline-none">
<FiUploadCloud className="w-8 h-8 text-gray-400" />
<span className="mt-2 text-sm text-gray-600 truncate">{fileName}</span>
<span className="text-xs text-blue-500">Click to select a file (PDF or DOCX)</span>
<input type="file" onChange={handleFileChange} accept=".pdf,.docx" className="hidden" />
</label>
<button
type="submit"
disabled={loading}
className="w-full px-4 py-2 mt-4 font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition disabled:bg-gray-400"
>
{loading ? 'Uploading...' : 'Upload Resume'}
</button>
</form>
{error && (
<div className="flex items-center mt-2 text-sm text-red-600">
<FiAlertCircle className="mr-1" /> {error}
</div>
)}
</div>
);
};
export default ResumeUploader;
Backend (Flask resume_routes.py): I'm using Flask-RESTful and reqparse to catch the file.
//resume_routes.py
from flask_restful import Resource, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity
from werkzeug.datastructures import FileStorage
from extensions import mongo
from utils.pdf_extractor import extract_text_from_file
from services.resume_parser import parse_resume
from services.scoring_engine import calculate_suitability_score
from models.user_model import User
import os
import re
# Create a directory for uploads
UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
upload_parser = reqparse.RequestParser()
upload_parser.add_argument('resume', type=FileStorage, location='files', required=True, help="Resume file is required")
analyze_parser = reqparse.RequestParser()
analyze_parser.add_argument('job_description', type=str, required=True, help="Job description is required")
class ResumeUpload(Resource):
@jwt_required()
def post(self):
data = upload_parser.parse_args()
resume_file = data['resume']
identity = get_jwt_identity()
user = User.find_by_email(identity['email'])
if not user:
return {'message': 'User not found'}, 404
filename = re.sub(r'[^a-zA-Z0-9._-]', '_', resume_file.filename)
file_path = os.path.join(UPLOAD_FOLDER, f"{user['_id']}_{filename}")
try:
resume_file.save(file_path)
text = extract_text_from_file(file_path)
if text is None:
return {'message': 'Unsupported file type or error reading file'}, 400
parsed_data = parse_resume(text)
mongo.db.resumes.update_one(
{'user_id': user['_id']},
{'$set': {
'user_id': user['_id'],
'filename': filename,
'text': parsed_data['full_text'],
'parsed': parsed_data
}},
upsert=True
)
except Exception as e:
return {'message': f"An error occurred: {str(e)}"}, 500
finally:
if os.path.exists(file_path):
os.remove(file_path)
return {'message': 'Resume uploaded and parsed successfully', 'data': parsed_data}, 201
class ResumeAnalyze(Resource):
@jwt_required()
def post(self):
identity = get_jwt_identity()
if identity['role'] != 'recruiter':
return {'message': 'Access forbidden: Recruiters only'}, 403
data = analyze_parser.parse_args()
job_description = data['job_description']
candidates = []
for resume in mongo.db.resumes.find():
resume_text = resume.get('text', '')
parsed_data = resume.get('parsed', {})
score, matched, missing = calculate_suitability_score(resume_text, job_description)
candidates.append({
'name': parsed_data.get('name', 'Unknown'),
'email': parsed_data.get('email', 'Unknown'),
'score': score,
'matched_skills': matched,
'missing_skills': missing
})
ranked_candidates = sorted(candidates, key=lambda x: x['score'], reverse=True)
return {'message': 'Analysis complete', 'results': ranked_candidates}, 200
def init_resume_routes(api):
api.add_resource(ResumeUpload, '/resume/upload')
api.add_resource(ResumeAnalyze, '/resume/analyze')