Table of contents

How do I handle multipart form uploads with Guzzle?

Multipart form uploads are essential for sending files and complex data through HTTP requests. Guzzle, the popular PHP HTTP client, provides robust support for multipart/form-data encoding, making it easy to upload files, submit complex forms, and handle various content types in a single request. This comprehensive guide will show you how to master multipart form uploads using Guzzle.

Understanding Multipart Form Data

Multipart form data (MIME type: multipart/form-data) is a special encoding that allows you to send different types of data, including files, in a single HTTP request. Unlike standard form encoding (application/x-www-form-urlencoded), multipart encoding can handle binary data efficiently and supports file uploads with metadata.

When to Use Multipart Forms

  • File uploads - Images, documents, videos, or any binary files
  • Mixed content - Combining text fields with file uploads
  • Large data - When form data exceeds URL length limits
  • Complex structures - Nested data or multiple content types

Basic Multipart File Upload

Single File Upload

The simplest way to upload a file using Guzzle's multipart support:

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client();

try {
    $response = $client->post('https://httpbin.org/post', [
        'multipart' => [
            [
                'name' => 'file',
                'contents' => fopen('/path/to/document.pdf', 'r'),
                'filename' => 'document.pdf'
            ]
        ]
    ]);

    echo "Upload successful: " . $response->getStatusCode();
} catch (RequestException $e) {
    echo 'Upload failed: ' . $e->getMessage();
}
?>

File Upload with Form Fields

Combining file uploads with regular form data:

<?php
$client = new Client();

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'title',
            'contents' => 'My Important Document'
        ],
        [
            'name' => 'description',
            'contents' => 'This document contains important information'
        ],
        [
            'name' => 'category',
            'contents' => 'legal'
        ],
        [
            'name' => 'document',
            'contents' => fopen('/path/to/document.pdf', 'r'),
            'filename' => 'important_document.pdf'
        ]
    ]
]);
?>

Advanced Multipart Upload Techniques

Multiple File Upload

Uploading multiple files in a single request:

<?php
$client = new Client();

$files = [
    '/path/to/document1.pdf',
    '/path/to/document2.docx',
    '/path/to/image.jpg'
];

$multipart = [];

// Add form fields
$multipart[] = [
    'name' => 'user_id',
    'contents' => '12345'
];

$multipart[] = [
    'name' => 'upload_batch',
    'contents' => 'batch_' . date('Y-m-d_H-i-s')
];

// Add multiple files
foreach ($files as $index => $filePath) {
    $multipart[] = [
        'name' => 'files[]',  // Array notation for multiple files
        'contents' => fopen($filePath, 'r'),
        'filename' => basename($filePath)
    ];
}

$response = $client->post('https://api.example.com/bulk-upload', [
    'multipart' => $multipart
]);
?>

File Upload with Custom Headers

Adding specific headers to uploaded files:

<?php
$client = new Client();

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'image',
            'contents' => fopen('/path/to/image.jpg', 'r'),
            'filename' => 'profile_image.jpg',
            'headers' => [
                'Content-Type' => 'image/jpeg',
                'Content-Disposition' => 'form-data; name="image"; filename="profile_image.jpg"'
            ]
        ],
        [
            'name' => 'thumbnail',
            'contents' => fopen('/path/to/thumb.jpg', 'r'),
            'filename' => 'thumbnail.jpg',
            'headers' => [
                'Content-Type' => 'image/jpeg'
            ]
        ]
    ],
    'headers' => [
        'Authorization' => 'Bearer your-api-token',
        'X-Upload-Source' => 'web-application'
    ]
]);
?>

Upload from Memory/String Content

Uploading data without creating temporary files:

<?php
$client = new Client();

// Generate CSV content in memory
$csvData = "Name,Email,Age\n";
$csvData .= "John Doe,john@example.com,30\n";
$csvData .= "Jane Smith,jane@example.com,25\n";

// Upload JSON data
$jsonData = json_encode([
    'users' => [
        ['id' => 1, 'name' => 'John'],
        ['id' => 2, 'name' => 'Jane']
    ]
]);

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'csv_file',
            'contents' => $csvData,
            'filename' => 'users.csv',
            'headers' => [
                'Content-Type' => 'text/csv'
            ]
        ],
        [
            'name' => 'json_file',
            'contents' => $jsonData,
            'filename' => 'data.json',
            'headers' => [
                'Content-Type' => 'application/json'
            ]
        ]
    ]
]);
?>

Handling Large File Uploads

Streaming Large Files

For large files, use streaming to avoid memory issues:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Stream;

$client = new Client([
    'timeout' => 300,  // 5 minutes for large uploads
    'read_timeout' => 300
]);

$fileStream = new Stream(fopen('/path/to/large_file.zip', 'r'));

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'large_file',
            'contents' => $fileStream,
            'filename' => 'large_archive.zip'
        ],
        [
            'name' => 'checksum',
            'contents' => hash_file('sha256', '/path/to/large_file.zip')
        ]
    ]
]);
?>

Progress Tracking

Monitor upload progress for large files:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\TransferStats;

$client = new Client();

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'video',
            'contents' => fopen('/path/to/video.mp4', 'r'),
            'filename' => 'presentation.mp4'
        ]
    ],
    'progress' => function ($downloadTotal, $downloadedBytes, $uploadTotal, $uploadedBytes) {
        if ($uploadTotal > 0) {
            $percentage = round(($uploadedBytes / $uploadTotal) * 100, 2);
            echo "Upload progress: {$percentage}%\r";
        }
    },
    'on_stats' => function (TransferStats $stats) {
        echo "\nUpload completed in: " . $stats->getTransferTime() . " seconds\n";
        echo "Upload speed: " . round($stats->getEffectiveUri()->getHost() ? 1 : 0) . "\n";
    }
]);
?>

Working with Different Content Types

Image Upload with Validation

Uploading and validating images before sending:

<?php
function validateAndUploadImage($imagePath, $client) {
    // Validate image
    $imageInfo = getimagesize($imagePath);
    if (!$imageInfo) {
        throw new Exception("Invalid image file");
    }

    $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!in_array($imageInfo['mime'], $allowedTypes)) {
        throw new Exception("Unsupported image type: " . $imageInfo['mime']);
    }

    // Check file size (5MB limit)
    $fileSize = filesize($imagePath);
    if ($fileSize > 5 * 1024 * 1024) {
        throw new Exception("File too large: " . round($fileSize / (1024 * 1024), 2) . "MB");
    }

    $response = $client->post('https://api.example.com/upload-image', [
        'multipart' => [
            [
                'name' => 'image',
                'contents' => fopen($imagePath, 'r'),
                'filename' => basename($imagePath),
                'headers' => [
                    'Content-Type' => $imageInfo['mime']
                ]
            ],
            [
                'name' => 'width',
                'contents' => (string)$imageInfo[0]
            ],
            [
                'name' => 'height',
                'contents' => (string)$imageInfo[1]
            ]
        ]
    ]);

    return $response;
}

$client = new Client();
try {
    $response = validateAndUploadImage('/path/to/image.jpg', $client);
    echo "Image uploaded successfully!";
} catch (Exception $e) {
    echo "Upload failed: " . $e->getMessage();
}
?>

Document Upload with Metadata

Uploading documents with rich metadata:

<?php
$client = new Client();

$documentPath = '/path/to/contract.pdf';
$metadata = [
    'title' => 'Service Agreement Contract',
    'author' => 'Legal Department',
    'version' => '2.1',
    'tags' => ['contract', 'legal', 'service'],
    'confidential' => true
];

$response = $client->post('https://api.example.com/documents', [
    'multipart' => [
        [
            'name' => 'document',
            'contents' => fopen($documentPath, 'r'),
            'filename' => basename($documentPath),
            'headers' => [
                'Content-Type' => 'application/pdf'
            ]
        ],
        [
            'name' => 'metadata',
            'contents' => json_encode($metadata),
            'headers' => [
                'Content-Type' => 'application/json'
            ]
        ],
        [
            'name' => 'upload_date',
            'contents' => date('c')  // ISO 8601 format
        ]
    ]
]);
?>

Error Handling and Validation

Comprehensive Error Handling

Robust error handling for multipart uploads:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\ConnectException;

function uploadWithRetry($filePath, $maxRetries = 3) {
    $client = new Client(['timeout' => 60]);
    $retryCount = 0;

    while ($retryCount < $maxRetries) {
        try {
            // Check file exists and is readable
            if (!file_exists($filePath) || !is_readable($filePath)) {
                throw new Exception("File not found or not readable: $filePath");
            }

            $response = $client->post('https://api.example.com/upload', [
                'multipart' => [
                    [
                        'name' => 'file',
                        'contents' => fopen($filePath, 'r'),
                        'filename' => basename($filePath)
                    ],
                    [
                        'name' => 'checksum',
                        'contents' => hash_file('md5', $filePath)
                    ]
                ]
            ]);

            // Success
            return json_decode($response->getBody(), true);

        } catch (ClientException $e) {
            $statusCode = $e->getResponse()->getStatusCode();

            if ($statusCode === 413) {
                throw new Exception("File too large for upload");
            } elseif ($statusCode === 415) {
                throw new Exception("Unsupported file type");
            } elseif ($statusCode === 422) {
                $errorBody = json_decode($e->getResponse()->getBody(), true);
                throw new Exception("Validation error: " . ($errorBody['message'] ?? 'Unknown validation error'));
            } else {
                throw new Exception("Client error: $statusCode");
            }

        } catch (ServerException $e) {
            $retryCount++;
            if ($retryCount >= $maxRetries) {
                throw new Exception("Server error after $maxRetries attempts: " . $e->getResponse()->getStatusCode());
            }

            // Exponential backoff
            sleep(pow(2, $retryCount));

        } catch (ConnectException $e) {
            $retryCount++;
            if ($retryCount >= $maxRetries) {
                throw new Exception("Connection failed after $maxRetries attempts");
            }

            sleep(5); // Wait before retry
        }
    }
}

// Usage
try {
    $result = uploadWithRetry('/path/to/document.pdf');
    echo "Upload successful. File ID: " . $result['file_id'];
} catch (Exception $e) {
    echo "Upload failed: " . $e->getMessage();
}
?>

Integration with Web Scraping Workflows

Combining with Authentication

When multipart uploads require authentication, especially in web scraping scenarios:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;

$cookieJar = new CookieJar();
$client = new Client(['cookies' => $cookieJar]);

// First, authenticate to get session cookies
$loginResponse = $client->post('https://example.com/login', [
    'form_params' => [
        'username' => 'your_username',
        'password' => 'your_password'
    ]
]);

// Now upload with authenticated session
$uploadResponse = $client->post('https://example.com/upload', [
    'multipart' => [
        [
            'name' => 'user_file',
            'contents' => fopen('/path/to/file.txt', 'r'),
            'filename' => 'data.txt'
        ]
    ]
]);

// For complex scenarios with JavaScript authentication, 
// consider using browser automation tools like Puppeteer
// for handling authentication in Puppeteer: /faq/puppeteer/how-to-handle-authentication-in-puppeteer
?>

Handling CSRF Tokens in Uploads

Many applications require CSRF tokens for file uploads:

<?php
use GuzzleHttp\Client;
use DOMDocument;
use DOMXPath;

$client = new Client();

// Get upload page to extract CSRF token
$pageResponse = $client->get('https://example.com/upload-form');
$html = $pageResponse->getBody()->getContents();

// Extract CSRF token
$dom = new DOMDocument();
@$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
$csrfToken = $xpath->query('//meta[@name="csrf-token"]/@content')->item(0)->nodeValue;

// Upload with CSRF token
$uploadResponse = $client->post('https://example.com/upload', [
    'multipart' => [
        [
            'name' => '_token',
            'contents' => $csrfToken
        ],
        [
            'name' => 'uploaded_file',
            'contents' => fopen('/path/to/file.pdf', 'r'),
            'filename' => 'document.pdf'
        ]
    ]
]);
?>

Performance Optimization

Concurrent Uploads

Uploading multiple files concurrently for better performance:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client();
$promises = [];

$files = [
    '/path/to/file1.jpg',
    '/path/to/file2.pdf',
    '/path/to/file3.docx'
];

// Create concurrent upload promises
foreach ($files as $index => $filePath) {
    $promises[] = $client->postAsync('https://api.example.com/upload', [
        'multipart' => [
            [
                'name' => 'file',
                'contents' => fopen($filePath, 'r'),
                'filename' => basename($filePath)
            ],
            [
                'name' => 'batch_id',
                'contents' => 'batch_' . date('Y-m-d_H-i-s')
            ]
        ]
    ]);
}

// Wait for all uploads to complete
try {
    $responses = Promise\settle($promises)->wait();

    foreach ($responses as $index => $response) {
        if ($response['state'] === 'fulfilled') {
            echo "File " . ($index + 1) . " uploaded successfully\n";
        } else {
            echo "File " . ($index + 1) . " upload failed: " . $response['reason']->getMessage() . "\n";
        }
    }
} catch (Exception $e) {
    echo "Concurrent upload error: " . $e->getMessage();
}
?>

Best Practices

1. File Validation Before Upload

Always validate files before attempting upload:

<?php
function validateFileForUpload($filePath, $options = []) {
    $maxSize = $options['max_size'] ?? 10 * 1024 * 1024; // 10MB default
    $allowedTypes = $options['allowed_types'] ?? ['pdf', 'jpg', 'png', 'docx'];

    if (!file_exists($filePath)) {
        throw new Exception("File does not exist: $filePath");
    }

    $fileSize = filesize($filePath);
    if ($fileSize > $maxSize) {
        throw new Exception("File too large: " . round($fileSize / (1024 * 1024), 2) . "MB");
    }

    $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
    if (!in_array($extension, $allowedTypes)) {
        throw new Exception("File type not allowed: $extension");
    }

    return true;
}
?>

2. Memory Management for Large Files

Use streaming for large files to avoid memory issues:

<?php
use GuzzleHttp\Psr7\LazyOpenStream;

$client = new Client();

// Use lazy stream for large files
$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'large_file',
            'contents' => new LazyOpenStream('/path/to/large_file.zip', 'r'),
            'filename' => 'archive.zip'
        ]
    ]
]);
?>

3. Proper Content-Type Detection

Automatically detect and set appropriate content types:

<?php
function getContentType($filePath) {
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $filePath);
    finfo_close($finfo);

    return $mimeType ?: 'application/octet-stream';
}

$client = new Client();
$filePath = '/path/to/document.pdf';

$response = $client->post('https://api.example.com/upload', [
    'multipart' => [
        [
            'name' => 'document',
            'contents' => fopen($filePath, 'r'),
            'filename' => basename($filePath),
            'headers' => [
                'Content-Type' => getContentType($filePath)
            ]
        ]
    ]
]);
?>

Debugging Multipart Uploads

Request Inspection

Debug multipart requests to understand what's being sent:

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
use GuzzleHttp\HandlerStack;

$stack = HandlerStack::create();

// Add debugging middleware
$stack->push(Middleware::mapRequest(function ($request) {
    echo "=== REQUEST DEBUG ===\n";
    echo "Method: " . $request->getMethod() . "\n";
    echo "URI: " . $request->getUri() . "\n";
    echo "Headers:\n";
    foreach ($request->getHeaders() as $name => $values) {
        echo "  $name: " . implode(', ', $values) . "\n";
    }
    echo "Body size: " . $request->getBody()->getSize() . " bytes\n";
    echo "=====================\n\n";

    return $request;
}));

$client = new Client(['handler' => $stack]);

$response = $client->post('https://httpbin.org/post', [
    'multipart' => [
        [
            'name' => 'test_file',
            'contents' => 'This is test content',
            'filename' => 'test.txt'
        ]
    ]
]);
?>

Conclusion

Guzzle's multipart support provides a powerful and flexible way to handle file uploads and complex form submissions in PHP applications. Whether you're uploading single files, handling multiple files concurrently, or integrating uploads into web scraping workflows, Guzzle's intuitive API makes these tasks straightforward.

Key takeaways for successful multipart uploads:

  • Always validate files before uploading
  • Use appropriate error handling and retry logic
  • Consider memory usage for large files
  • Implement progress tracking for better user experience
  • Properly handle authentication and CSRF tokens

For complex web scraping scenarios that involve JavaScript-heavy upload interfaces, consider combining Guzzle with browser automation tools like handling file downloads in Puppeteer or monitoring network requests in Puppeteer for comprehensive web automation solutions.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon