skip to content

Why does a customer avatar sent with Inertia's form.put() arrive empty in a Laravel controller, and how do you fix it?

level: seniorimportance: should knowfreq 36%

answer

  1. files switch the body to multipart
  2. PHP parses multipart only for POST
  3. post() plus _method: 'put'
  4. forceFormData without any file
  5. progress.percentage while uploading

basics

~20 s

A 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 s

When 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 lines
jsx
import { 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

for a junior

Know that a file in the form data makes Inertia send multipart, and that progress.percentage drives an upload bar.

for a middle

Explain why a multipart PUT arrives empty, how _method spoofing fixes it, and what forceFormData does and does not change.

for a senior

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.

for a principal

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