1

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')

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.