Skip to content

Instantly share code, notes, and snippets.

@DiazFarindra
Last active February 8, 2026 04:29
Show Gist options
  • Select an option

  • Save DiazFarindra/2aeb50ac988274b46de4da2ce16099c0 to your computer and use it in GitHub Desktop.

Select an option

Save DiazFarindra/2aeb50ac988274b46de4da2ce16099c0 to your computer and use it in GitHub Desktop.
Laravel middleware utility to make multipart/form-data work reliably for non-POST API methods (PUT, PATCH, DELETE).

AI generated content percentage: 15%

Parse Multipart Form-Data for Non-POST Methods (Laravel)

What problem this solves

In API development, multipart/form-data is usually fine on POST, but often breaks on PUT, PATCH, or DELETE.
Typical symptoms:

  • fields are empty in update endpoints
  • uploaded files are missing
  • team ends up forcing update flows to POST only

This code fixes that by manually parsing raw multipart payloads for non-POST requests, then rebuilding Laravel’s normal request structure.

Code overview

  • app/Http/Middleware/ParseMultipartFormData.php
    Main middleware.
    It runs only when request method is not GET, HEAD, or POST.
    If Content-Type contains multipart/form-data, it:

    • reads raw request body
    • parses it using FormDataProcessor
    • adds parsed inputs into $request->request
    • adds parsed files into $request->files
    • recursively converts file arrays into UploadedFile objects
  • app/Core/FormDataProcessor.php
    Multipart parser engine.
    It splits body by boundary, reads headers, detects field name + optional filename, and maps:

    • normal form values into $inputs
    • file content into temp files, returning file metadata (name, type, tmp_name, size, etc.)
  • app/Exceptions/ParsingMultipartFormDataExeption.php
    Custom exception used when parsing fails.

How to use this gist

  1. Register middleware (global or API group) in your HTTP kernel/pipeline:
\App\Http\Middleware\ParseMultipartFormData::class,
  1. Send requests using multipart/form-data with methods like PUT/PATCH:
curl -X PATCH https://api.example.com/users/10 \
  -F "name=John" \
  -F "avatar=@/path/avatar.jpg"
  1. In controller, access data normally:
$request->input('name');
$request->file('avatar');

No special controller logic is needed after middleware is registered.

<?php
namespace App\Core;
/*
* This class is designed to handle the parsing of multipart/form-data requests,
* commonly used for processing form submissions with file uploads.
*
* It provides functionality to parse and organize both non-file
* form data and uploaded files.
*/
final class FormDataProcessor
{
public array $inputs = [];
public array $files = [];
public function __construct(private string $content)
{
$this->parseContent($this->content);
}
private function parseContent(string $content): void
{
$parts = $this->getParts($content);
foreach ($parts as $part) {
$this->processContent($part);
}
}
private function getParts(string $content): array
{
$boundary = $this->getBoundary($content);
if (is_null($boundary)) {
return [];
}
$parts = explode($boundary, $content);
return array_filter($parts, function (string $part): bool {
return mb_strlen($part) > 0 && $part !== "--\r\n";
});
}
private function getBoundary(string $content): ?string
{
$firstNewLinePosition = strpos($content, "\r\n");
return $firstNewLinePosition ? substr($content, 0, $firstNewLinePosition) : null;
}
private function processContent(string $content): void
{
$content = ltrim($content, "\r\n");
[$rawHeaders, $rawContent] = explode("\r\n\r\n", $content, 2);
$headers = $this->parseHeaders($rawHeaders);
if (isset($headers['content-disposition'])) {
$this->parseContentDisposition($headers, $rawContent);
}
}
private function parseHeaders(string $headers): array
{
$data = [];
$headers = explode("\r\n", $headers);
foreach ($headers as $header) {
[$name, $value] = explode(':', $header);
$name = strtolower($name);
$data[$name] = ltrim($value, ' ');
}
return $data;
}
private function parseContentDisposition(array $headers, string $content): void
{
$content = substr($content, 0, strlen($content) - 2);
preg_match('/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches);
$fieldName = $matches[1];
$fileName = $matches[3] ?? null;
if (is_null($fileName)) {
$input = $this->transformContent($fieldName, $content);
$this->inputs = array_merge_recursive($this->inputs, $input);
} else {
$file = $this->storeFile($fileName, $headers['content-type'], $content);
$file = $this->transformContent($fieldName, $file);
$this->files = array_merge_recursive($this->files, $file);
}
}
private function transformContent(string $name, mixed $value): array
{
parse_str($name, $parsedName);
$transform = function (array $array, mixed $value) use (&$transform) {
foreach ($array as &$val) {
$val = is_array($val) ? $transform($val, $value) : $value;
}
return $array;
};
return $transform($parsedName, $value);
}
private function storeFile(string $name, string $type, string $content): array
{
$tempDirectory = sys_get_temp_dir();
$tempName = tempnam($tempDirectory, 'tempdir');
file_put_contents($tempName, $content);
register_shutdown_function(function () use ($tempName): void {
if (file_exists($tempName)) {
unlink($tempName);
}
});
return [
'name' => $name,
'type' => $type,
'tmp_name' => $tempName,
'error' => 0,
'size' => filesize($tempName),
];
}
}
<?php
namespace App\Http\Middleware;
use App\Core\FormDataProcessor;
use App\Exceptions\ParsingMultipartFormDataException;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
/*
* Content-Type: multipart/form-data - only works for POST requests. All others fail, this is a bug in PHP since 2011.
* See comments here: https://github.com/laravel/framework/issues/13457
*
* This middleware converts all multi-part/form-data for NON-POST requests, into a properly formatted
* request variable for Laravel 5.6. It uses the ParseInputStream class, found here:
* https://gist.github.com/devmycloud/df28012101fbc55d8de1737762b70348
*/
class ParseMultipartFormData
{
private array $disallowMethods = [
Request::METHOD_GET,
Request::METHOD_HEAD,
Request::METHOD_POST,
];
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
try {
if (! in_array($request->getRealMethod(), $this->disallowMethods)) {
$headers = $request->headers;
$contentType = $headers->get('content-type');
if (preg_match('/multipart\/form-data/', $contentType)) {
$content = $request->getContent();
$static = new FormDataProcessor($content);
$request->request->add($static->inputs);
$request->files->add($static->files);
$files = $this->filesProcessor($request->files->all());
$request->files->replace($files);
}
}
return $next($request);
} catch (\Throwable $th) {
info($th);
throw new ParsingMultipartFormDataException(message: $th->getMessage());
}
}
private function filesProcessor(array $files): array
{
try {
$result = [];
foreach ($files as $key => $file) {
$result[$key] = is_array($file) ? $this->filesProcessor($file) : UploadedFile::createFromBase($file, true);
}
return $result;
} catch (\Throwable $th) {
info($th);
throw new ParsingMultipartFormDataException(message: $th->getMessage());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment