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.