for several days I have problem uploading an image with Laravel api. I was sending request via React's modified axios. Code is as follows.
axios-client.js
import axios from "axios";
const axiosClient = axios.create({
baseURL : `${import.meta.env.VITE_API_BASE_URL}/api`
});
axiosClient.interceptors.request.use((config) => {
const token = localStorage.getItem('ACCESS_TOKEN');
config.headers.Authorization = `Bearer ${token}`;
if (config.data instanceof FormData) {
console.log('Form Data accepted')
config.headers['Content-Type'] = 'multipart/form-data';
}
return config;
});
axiosClient.interceptors.response.use((response) => {
return response;
}, (error) => {
try {
const { response } = error;
if (response.status === 401) {
localStorage.removeItem('ACCESS_TOKEN');
}
} catch (e) {
console.log(e);
}
throw error;
});
export default axiosClient;
React's component
BrandForm.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import axiosClient from '../../axios-client';
function BrandForm() {
const {id} = useParams();
const navigate = useNavigate();
const [loading,setLoading] = useState(false);
const [errors, setErrors] = useState(null);
const [brand,setBrand] = useState({
id: null,
name: '',
logo: null
});
const config = {headers: {'Content-Type': 'multipart/form-data'}}
if (id){
useEffect(()=>{
setLoading(true);
axiosClient.get(`/car-make/${id}`)
.then(({data})=>{
console.log(data)
setBrand(data);
setLoading(false)
})
.catch((e)=>{
console.log(e)
setLoading(false);
})
},[]);
}
const onSubmit = (e)=>{
e.preventDefault();
setErrors(null);
if (brand.id){
axiosClient.put(`/car-make/${brand.id}`, brand, config)
.then(({data})=>{
//TODO show notification
console.log('response update', data)
navigate(path);
})
.catch( (err) => {
console.log(err)
const response = err.response;
if(response && response.status ===422){
setErrors(response.data.errors);
}
});
}
else {
axiosClient.post(`/car-make`, brand, config)
.then(()=>{
//TODO show notification
console.log('response create', data)
navigate(path);
})
.catch( (err) => {
const response = err.response;
if(response && response.status ===422){
setErrors(response.data.errors);
}
});
}
}
return (
<div>
{brand?.id ? <h1>Edit Brand: {brand.name}</h1> : <h1>New Brand</h1>}
<div className="card animated fadeInDown">
{loading && <div className="text-center">Loading...</div>}
{errors && (
<div className="alert">
{Object.keys(errors).map((key) => (
<p key={key}>{errors[key][0]}</p>
))}
</div>
)}
{!loading && (
<form onSubmit={onSubmit}>
<input
value={brand?.name}
onChange={(e) => setBrand({ ...brand, name: e.target.value })}
type="text"
placeholder="Name"
/>
<input
onChange={(e) => setBrand({ ...brand, logo: e.target.files[0] })}
type="file"
placeholder="Logo"
/>
<button className="btn" style={{ marginTop: '20px' }}>Save</button>
</form>
)}
</div>
</div>
);
}
export default BrandForm;
Laravel's
api.php
Route::middleware('auth:sanctum')->group(function(){
Route::put('/transfer/execute/{id}', [TransferController::class,'execute']);
Route::put('/transfer/cancel/{id}', [TransferController::class,'cancel']);
Route::apiResource('/transfer', TransferController::class);
Route::apiResource('/vehicle', VehicleController::class);
Route::apiResource('/car-make', CarMakeController::class);
Route::apiResource('/car-model', CarModelController::class);
Route::get('/companies/partner',[CompanyController::class,'indexPartner']);
Route::get('/companies/fleet',[CompanyController::class,'indexFlota']);
Route::apiResource('/companies', CompanyController::class);
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::post('/logout', [AuthController::class,'logout']);
Route::apiResource('/users', UserController::class);
});
Route::post('/signup', [AuthController::class,'signup']);
Route::post('/login', [AuthController::class,'login']);
Model
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CarMake extends Model
{
use HasFactory;
protected $fillable = ['name', 'logo'];
protected $primarykey = "id";
public function carModels()
{
return $this->hasMany(CarModel::class,'make');
}
}
Controller
.
.
.
public function update(UpdateCarMakeRequest $request, CarMake $carMake)
{
Log::info('Update method called');
Log::info('Update Brand Request - Raw Request Data:', $request->all());
$data = $request->validated();
if ($request->hasFile('logo')) {
$file = $request->file('logo');
$fileName = time() . '_' . $file->getClientOriginalName();
$filePath = $file->storeAs('logo', $fileName, 'public');
$data['logo'] = $filePath;
} else {
unset($data['logo']);
}
Log::info('Update Brand Request - Validated Data:', $data);
$carMake->update($data);
return new CarMakeResource($carMake);
}
.
.
.
Update Request
class UpdateCarMakeRequest extends FormRequest
{
public function authorize()
{
return true; // Adjust as needed
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'logo' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
];
}
protected function failedValidation(Validator $validator)
{
Log::error('Validation errors:', $validator->errors()->toArray());
throw new HttpResponseException(response()->json([
'errors' => $validator->errors()
], 422));
}
}
Reource
class CarMakeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id'=> $this->id,
'name' => $this->name,
'logo'=> $this->logo,
];
}
}
The Log file looks like this after trying to edit
[2024-07-18 05:29:50] local.ERROR: Validation errors: {"name":["The name field is required."]}
[2024-07-18 05:44:36] local.ERROR: Validation errors: {"name":["The name field is required."]}
Note: when I try updating CarMake without an image and without config parameter in axios request it works fine
Thanks in advance for your effort!
Your request interceptor data handling is completely redundant. Axios already handles FormData
payloads and does so correctly. Yours is missing the mime boundary token.
Remove this and let Axios and your browser set the appropriate content-type header
if (config.data instanceof FormData) { console.log('Form Data accepted') config.headers['Content-Type'] = 'multipart/form-data'; }
You also cannot embed files in a JSON document. Create a FormData
instance for your request payload, populate the data and send that
const onSubmit = async (e) => {
e.preventDefault();
setErrors(null);
const fd = new FormData();
Object.entries(brand).forEach(([key, value]) => {
if (value !== null) fd.append(key, value);
});
try {
if (brand.id) {
await axiosClient.put(`/car-make/${encodeURIComponent(brand.id)}`, fd);
} else {
await axiosClient.post(`/car-make`, fd);
}
// use your dev-tools to inspect responses, not console.log()
navigate(path);
} catch (err) {
console.warn(err);
const response = err.response;
if (response && response.status === 422) {
setErrors(response.data.errors);
}
}
};
For Laravel to handle multipart/form-data
PUT requests, you might need to resort to axios.post()
and append _method=PUT
await axios.post(`/car-make/${encodeURIComponent(brand.id)}`, fd, {
params: {
_method: 'PUT',
},
});