angularlaravelhttp-status-code-422

How to resolve the PUT 422 (Unprocessable Content) error


I am working on a simple university project, a movie catalog, but I am stuck on a problem that I can't find a solution for. When trying to update a movie, I get a 422 error, and I can't figure out the problem. I created my API using Laravel 10, and I'm sharing my controller with you, specifically the update method, as that's where I'm getting the error, along with my route:

MovieController.php

  public function updateMovie(Request $request, $id)
    {
        Log::info('Request data:', $request->all());
        Log::info('Request files:', $request->file());
    
        if (!$request->has('title')) {
            Log::error('Title is missing');
        }
        if (!$request->has('synopsis')) {
            Log::error('Synopsis is missing');
        }
        if (!$request->has('year')) {
            Log::error('Year is missing');
        }
    
        $validatedData = $request->validate([
            'title' => 'required|string|max:255',
            'synopsis' => 'required|string',
            'year' => 'required|integer|min:1900|max:' . date('Y'),
            'cover' => 'nullable|file|mimes:jpeg,png,jpg,gif|max:2048',
        ]);

api.php

Route::put('/movies/{id}', [MovieController::class, 'updateMovie']);

I think the problem is not with my API but with my client. I'm using Angular 16 for the frontend. Now, I'm going to show you my edit component and the configuration of my service:

movie.edit.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MovieService } from '../../services/movie.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-movie-edit',
  templateUrl: './movie-edit.component.html',
  styleUrls: ['./movie-edit.component.scss']
})
export class MovieEditComponent implements OnInit {
  movieForm: FormGroup;
  movieId!: number;
  movie: any;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private movieService: MovieService,
    private fb: FormBuilder
  ) {
    this.movieForm = this.fb.group({
      title: ['', Validators.required],
      synopsis: ['', Validators.required],
      year: ['', [Validators.required, Validators.min(1900), Validators.max(new Date().getFullYear())]],
      cover: [null]
    });
  }

  ngOnInit(): void {
    this.movieId = Number(this.route.snapshot.paramMap.get('id'));
    this.getMovieDetails();
  }

  getMovieDetails(): void {
    this.movieService.getMovieById(this.movieId).subscribe({
      next: (data) => {
        this.movie = data;
        this.movieForm.patchValue({
          title: this.movie.title,
          synopsis: this.movie.synopsis,
          year: this.movie.year
        });
      },
      error: (error) => {
        console.error('Error fetching movie details', error);
      }
    });
  }

  onFileChange(event: any): void {
    if (event.target.files.length > 0) {
      const file = event.target.files[0];
      this.movieForm.patchValue({
        cover: file
      });
    }
  }

  onSubmit(): void {
    if (this.movieForm.valid) {
      const formData = new FormData();
      const title = this.movieForm.get('title')?.value || '';
      const synopsis = this.movieForm.get('synopsis')?.value || '';
      const year = this.movieForm.get('year')?.value;
      const cover = this.movieForm.get('cover')?.value;
  
      console.log('Title:', title);
      console.log('Synopsis:', synopsis);
      console.log('Year:', year);
      console.log('Cover:', cover);
  
      formData.append('title', title);
      formData.append('synopsis', synopsis);
      if (year !== undefined && year !== null) {
        formData.append('year', year.toString());
      }
      if (cover) {
        formData.append('cover', cover);
      }
  
      formData.forEach((value, key) => {
        console.log(key, value);
      });
  
      this.movieService.updateMovie(this.movieId, formData).subscribe({
        next: () => {
          this.router.navigate(['/movie', this.movieId]);
        },
        error: (error) => {
          console.error('Error updating movie', error);
        }
      });
    }
  }
}

movie.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class MovieService {
  private apiUrl = 'http://localhost:8000/api/movies';

  constructor(private http: HttpClient) {}

  getAllMovies(): Observable<any> {
    return this.http.get(this.apiUrl);
  }

  getMovieById(id: number): Observable<any> {
    return this.http.get(`${this.apiUrl}/${id}`);
  }

  createMovie(movie: any): Observable<any> {
    const formData = new FormData();
    formData.append('title', movie.title);
    formData.append('synopsis', movie.synopsis);
    formData.append('year', movie.year.toString());
    formData.append('cover', movie.cover);

    return this.http.post(this.apiUrl, formData);
  }

  updateMovie(id: number, formData: FormData): Observable<any> {
    return this.http.put(`${this.apiUrl}/${id}`, formData);
  }

  deleteMovie(id: number): Observable<any> {
    return this.http.delete(`${this.apiUrl}/${id}`);
  }
}

It seems that the data is not reaching the backend; there appears to be an issue with how the FormData is being sent from Angular.

This is what it shows me in the log file:

[2024-10-08 00:33:17] local.INFO: Request data:  
[2024-10-08 00:33:17] local.INFO: Request files:  
[2024-10-08 00:33:17] local.ERROR: Title is missing  
[2024-10-08 00:33:17] local.ERROR: Synopsis is missing  
[2024-10-08 00:33:17] local.ERROR: Year is missing  

Please help, I need to submit this activity in two weeks.


Solution

  • You could try POST method with parameter _method="PUT" when submit as PHP not recognize the multipart-form-data correctly for uploading files with PUT method.

    So change your submitting form method to POST and add in

    formData.append('_method', 'PUT');