A Laravel dating app rejects large profile photos, sometimes with a 413 PostTooLargeException and sometimes with 'The photo failed to upload.'; why two symptoms?
answer
- two PHP limits, two layers
- ValidatePostSize checks CONTENT_LENGTH
- PostTooLargeException is an HTTP 413
- upload_max_filesize 2M, post_max_size 8M
- validator adds the uploaded failure
basics
~20 sA body over post_max_size is stopped by the global ValidatePostSize middleware with a 413 PostTooLargeException. A file over upload_max_filesize but inside post_max_size reaches the controller as an invalid UploadedFile, and validation reports it as failed to upload.
solid answer
~40 sPHP enforces two limits, defaulting to `upload_max_filesize = 2M` per file and `post_max_size = 8M` per request body. Laravel's global `ValidatePostSize` middleware compares the `CONTENT_LENGTH` server value with `post_max_size` and throws `Illuminate\Http\Exceptions\PostTooLargeException`, an HTTP 413, before routing, so a 9 MB photo never reaches the controller. A 5 MB photo fits the body limit but breaks the per-file limit: PHP records `UPLOAD_ERR_INI_SIZE` and leaves no temporary file, so `file('photo')` is an invalid `UploadedFile`, `hasFile()` is false, and the validator adds the `uploaded` failure, which renders as 'The photo failed to upload.' Fix it by raising both ini values together, upload limit below body limit, plus any reverse-proxy body limit, then set the app's own validation maximum lower.
code
ini · 4 lines; php.ini for the PHP-FPM pool that serves the app
upload_max_filesize = 10M
post_max_size = 55M
max_file_uploads = 20go deeper
Remember that PHP has a per-file limit and a whole-request limit, and that both can block uploads before your code runs.
Map each symptom to its layer: ValidatePostSize and a 413 for the body limit, an invalid UploadedFile and the uploaded failure for the file limit.
Size proxy, PHP and validation limits as a stack, keep the per-file limit below the body limit, and give API clients actionable 413s.
Decide when media should bypass application servers with direct-to-storage uploads, trading simplicity for scalability and cost.
## Two limits, two failure points Large uploads on a dating app fail in two visibly different ways because PHP enforces two independent limits, and Laravel meets each at a different layer. | PHP setting | Default | Scope | |---|---|---| | `upload_max_filesize` | `2M` | each uploaded file | | `post_max_size` | `8M` | the whole request body, all fields and files | | `max_file_uploads` | `20` | number of files per request | These are the defaults compiled into PHP and shipped in its `php.ini-production`; hosts often change them. ## Symptom 1: 413 PostTooLargeException Laravel 13's default global middleware stack includes `Illuminate\Http\Middleware\ValidatePostSize`. It converts `ini_get('post_max_size')` to bytes and, when the request's `CONTENT_LENGTH` exceeds it, throws **`Illuminate\Http\Exceptions\PostTooLargeException`**, an `HttpException` with status **413**. This happens before routing and before your controller or validation run. So a 9 MB photo against the 8 MB default body limit yields a 413 page or JSON error. The controller never sees the request, which is why a validation message is not produced. ## Symptom 2: "The photo failed to upload." A 5 MB photo is under `post_max_size`, so `ValidatePostSize` lets it through. PHP, however, applies `upload_max_filesize` to the file part: 1. It stops writing the file, records the error code `UPLOAD_ERR_INI_SIZE`, and leaves the temporary name empty. 2. Laravel wraps the entry as an `Illuminate\Http\UploadedFile` whose path is empty and whose `getError()` is that code. 3. `$request->hasFile('photo')` returns false, because it requires a non-empty path, and `isValid()` returns false. 4. When the field has file or implicit validation rules, the validator notices the invalid `UploadedFile` and records the **`uploaded`** failure. The default English message is "The :attribute failed to upload.", which renders as "The photo failed to upload." The message is generic because the validator does not look at the error code; `getError()` on the file tells you the real cause. ## Fixing it properly 1. **Decide the product limit**, say 10 MB per photo and up to five photos per request. 2. **Raise PHP's limits to fit**: `upload_max_filesize` at least the per-photo limit, `post_max_size` comfortably above the largest total body, and `max_file_uploads` above the count. Keeping `upload_max_filesize` below `post_max_size` makes oversized single files fail as a validation error rather than as a 413. 3. **Check every layer in front of PHP.** Web servers and reverse proxies usually have their own request body limit and answer 413 themselves, before PHP or Laravel run. 4. **Set the app's validation maximum below PHP's limits**, so users get a precise, translated message instead of the generic "failed to upload". 5. **Handle the 413 for API clients**: the exception renders as JSON for requests that expect JSON, so mobile clients receive a 413 status they can act on. ## Diagnosing in production - A 413 with no application log line points at a proxy limit. - A 413 rendered by Laravel points at `post_max_size` and `ValidatePostSize`. - A "failed to upload" validation error points at `upload_max_filesize`, a partial upload, or a missing temporary directory, all distinguishable through `getError()`. - Remember that PHP's CLI and FPM can read different ini files, so `php -i` on the command line may not show what the web workers use. For very large media, direct-to-storage uploads that bypass PHP entirely avoid these limits, at the cost of a different upload flow. ## A quick triage table | What the user sees | Layer that answered | Setting to look at | |---|---|---| | 413 page not rendered by Laravel | web server or proxy | its request body limit | | 413 rendered by Laravel | `ValidatePostSize` | `post_max_size` | | "The photo failed to upload." | validator, on an invalid `UploadedFile` | `upload_max_filesize` or `getError()` | | The app's own size message | validator, on a valid file | the validation maximum | Reading the symptoms in this order usually finds the culprit in minutes, without guessing at every limit at once.
- Why does a 9 MB photo against the default limits never produce a validation error message?The request body exceeds `post_max_size` (8M by default), so the global `ValidatePostSize` middleware throws `PostTooLargeException` with status 413 before routing. Validation runs later, inside the controller or a form request, so it never gets the chance to add a message.
- Why keep upload_max_filesize below post_max_size rather than equal to it?The body also carries other fields and multipart overhead. If the per-file limit equals the body limit, a file near the limit pushes the body over `post_max_size`, and the user gets a 413 page instead of a friendly validation error. A lower per-file limit keeps single oversized files inside the validation path.
saying these in an interview costs you the question
- Validation max rules stop files before PHP's limits apply
- A 413 always means the web server rejected the request
- Raising upload_max_filesize alone fixes large uploads
- A file over upload_max_filesize makes $request->file() null
- The failed to upload message states which PHP limit was hit