Why does a customer avatar sent with Inertia's form.put() arrive empty in a Laravel controller, and how do you fix it?
answer
- files switch the body to multipart
- PHP parses multipart only for POST
- post() plus _method: 'put'
- forceFormData without any file
- progress.percentage while uploading
basics
~20 sA File in the form data makes Inertia send multipart/form-data, and PHP only parses multipart bodies for POST, so a real PUT reaches Laravel with no fields or files; submit with post() and _method: 'put' so Laravel spoofs the method.
solid answer
~40 sWhen the form data contains a `File`, `Blob` or `FileList`, Inertia converts the payload to `FormData` and sends `multipart/form-data`; `forceFormData: true` forces that even without files. PHP fills `$_POST` and `$_FILES` from a multipart body only when the method is POST, and Laravel 13 does not re-parse other methods, so `form.put()` with an avatar arrives with `$request->file('avatar')` null and validation reporting every field missing. The fix is method spoofing: send a POST whose data includes `_method: 'put'` (in the data or through `transform()`), and Laravel routes it to the `Route::put` action. While the upload runs, `form.progress` holds the progress event, so `progress.percentage` can feed a `<progress>` bar; it returns to `null` in `onFinish`.
code
jsx · 20 linesimport { useForm } from '@inertiajs/react'
export default function Edit({ customer }) {
const form = useForm({ name: customer.name, avatar: null })
function submit(e) {
e.preventDefault()
form.transform((data) => ({ ...data, _method: 'put' }))
form.post(`/customers/${customer.id}`)
}
return (
<form onSubmit={submit}>
<input type="file" onChange={(e) => form.setData('avatar', e.target.files[0])} />
{form.progress && <progress value={form.progress.percentage} max="100" />}
{form.errors.avatar && <p>{form.errors.avatar}</p>}
<button disabled={form.processing}>Save</button>
</form>
)
}go deeper
Know that a file in the form data makes Inertia send multipart, and that progress.percentage drives an upload bar.
Explain why a multipart PUT arrives empty, how _method spoofing fixes it, and what forceFormData does and does not change.
Diagnose the regression when an upload is added to an existing PUT form, and design edit forms so optional files, arrays and spoofed methods behave consistently.
Decide where uploads belong, such as direct-to-storage uploads versus through the app, weighing request size limits, timeouts and the effect on form design.
## The symptom A customer edit page lets the user replace an avatar. The page uses **Inertia**'s `useForm` with `{ name, email, avatar: null }`, the file input calls `setData('avatar', e.target.files[0])`, and Save calls `form.put('/customers/42')`. The controller's validation then fails with **every** field reported as required, or `$request->file('avatar')` is `null`, even though the browser's network panel shows the file being sent. ## Why it happens Two mechanisms meet here. **1. Inertia switches to multipart when it sees a file.** Before sending a visit, Inertia's client checks the data for `File`, `Blob` or `FileList` values at any depth. If it finds one, it converts the whole payload into a browser **`FormData`** object, so the request body is `multipart/form-data`. Arrays inside that `FormData` use indexed keys (`tags[0]`) by default, controlled by the client config key `form.forceIndicesArrayFormatInFormData`. **2. PHP only parses multipart bodies for POST.** PHP's request startup reads form bodies into `$_POST` and `$_FILES` only when the request method is POST. A PUT or PATCH with a multipart body is left unparsed, and Laravel 13 does not parse it either. The controller sees an empty input bag and no uploaded files. The result is a request that looks right in the browser and arrives empty on the server. ## The fix: method spoofing Laravel supports **form method spoofing**: a POST request carrying a `_method` field is treated as that method for routing. So the client sends a POST, PHP parses the multipart body as usual, and Laravel still dispatches to the `Route::put('/customers/{customer}', ...)` action. Three equivalent ways to do it with Inertia: - include the field in the initial data: `useForm({ _method: 'put', name, email, avatar: null })` and call `form.post(url)`; - keep the data clean and add it at submit time: `form.transform((data) => ({ ...data, _method: 'put' }))`, then `form.post(url)`; - with the router directly: `router.post(url, { _method: 'put', avatar })`. With the `<Form>` component, use `method="post"` and a hidden `<input name="_method" value="put">`. | Approach | Body parsed by PHP? | Route matched | |---|---|---| | `form.put(url)` with a file | no | `PUT`, but input is empty | | `form.post(url)` with `_method: 'put'` | yes | `PUT` via spoofing | | `form.put(url)` without files | yes (URL-encoded or JSON) | `PUT` | The last row is why the bug appears only after someone adds an upload to a form that worked for months. ## forceFormData The visit option **`forceFormData: true`** tells Inertia to build `FormData` even when no file is present. It helps when a server endpoint expects multipart consistently, for example an avatar field that is sometimes empty. It does not change which method PHP parses, so it does not fix a PUT. ## Showing upload progress `useForm` updates **`progress`** from the visit's progress events: 1. `null` before the upload starts; 2. an object with `percentage` (plus loaded and total bytes) while the body uploads; 3. `null` again when `onFinish` runs. In React: `{form.progress && <progress value={form.progress.percentage} max="100" />}`. The top-of-page progress bar still runs unless `showProgress: false` is passed. ## Checklist for an edit form with an upload 1. Initialise the file field as `null` so the form can be saved without a new file. 2. Submit with `post()` and add `_method: 'put'` or `'patch'` to match the route. 3. Show `progress.percentage` while `progress` is set, and disable the button on `processing`. 4. Display `errors.avatar` next to the file input; the server's file rules produce it like any other field. 5. Reset the file input after success, because a file input cannot be restored from state. ## What stays on the server side Storing the file (`$request->file('avatar')->store(...)`), size and mime rules, and disk configuration belong to Laravel's upload and storage features. The Inertia-specific part of the answer is the multipart switch, the POST-only parsing and the `_method` spoof.
- Does forceFormData: true fix the empty PUT?No. `forceFormData` only makes Inertia build a multipart `FormData` body even without files. The problem is that PHP does not parse multipart bodies for PUT, so forcing multipart on a PUT makes the empty-input problem appear even without a file.
- Why did the same form work before the avatar field was added?Without a file, Inertia sends the data as a JSON body, which Laravel reads for any method. Adding a `File` switches the body to multipart, which PHP parses only for POST, so the PUT suddenly arrives empty.
saying these in an interview costs you the question
- Setting forceFormData: true makes a multipart PUT readable
- Inertia uploads files through a separate endpoint automatically
- Laravel parses multipart bodies for PUT like any other method
- Files must be base64-encoded before Inertia can send them
- progress stays filled after the upload until you clear it