Save an image from an external URL in Laravel
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Save an Image from an External URL in Laravel: Mastering HTTP Streams and Storage
As developers, we often need to integrate external data into our application. One common requirement is downloading content—like images or files—from an external URL and saving them within our local storage system, typically cloud storage like Amazon S3 via Laravel's Filesystem abstraction.
The scenario you presented involves fetching content using file_get_contents() and attempting to save it. While this approach seems straightforward, as demonstrated by the "Malformed UTF-8 characters" error, it highlights a common pitfall when dealing with HTTP streams and character encoding in PHP, especially when fetching data from complex CDN URLs.
This post will diagnose why your original code failed and provide a robust, Laravel-idiomatic solution for securely downloading and storing external images.
The Pitfall: Why file_get_contents() Fails on External URLs
Your attempt to use file_get_contents($url) followed by saving the result into the storage disk resulted in an encoding error. This often happens because external image URLs (especially those served via CDNs) return data that might contain non-standard headers, metadata, or improperly encoded characters that standard file reading functions struggle to interpret correctly when immediately piped into a local file system operation.
When dealing with HTTP requests for large files or binary data, relying solely on basic PHP functions can be fragile. A more reliable method involves using dedicated HTTP clients like Guzzle or cURL, which allow you to manage the response stream and error handling much more effectively, ensuring that the raw binary content is correctly handled before it hits the storage layer.
The Robust Solution: Using Streams for Reliable Downloads
Instead of reading the entire file into a variable immediately, the most reliable method is to stream the data directly from the HTTP request into the destination file handler provided by Laravel's Storage facade. This leverages the underlying stream capabilities efficiently.
We will use PHP's stream functions to handle the download safely and then let Laravel manage the final placement in S3 or local disk.
Step-by-Step Implementation
Here is how we can refactor your function to reliably handle external image downloads:
use Illuminate\Support\Facades\Storage;
public function uploadFromUrl(string $url, string $path, string $filename): string
{
// 1. Determine the storage disk based on environment settings
$storage_disk = 'local' === config('app.env') ? 'public' : 's3';
// 2. Use stream context to safely fetch content (more robust than raw file_get_contents)
// We use fopen to open a stream to the remote URL
$stream = fopen($url, 'r');
if (!$stream) {
throw new \Exception("Failed to open stream from URL: " . $url);
}
// 3. Save the stream directly to the storage disk
// Laravel's putStream method handles writing the incoming data efficiently.
Storage::disk($storage_disk)->putStream($path . '/' . $filename, $stream);
// 4. Close the stream immediately after reading/writing
fclose($stream);
// 5. Retrieve the final URL (handling S3 specific URL formatting)
if ('production' === config('app.env')) {
$url = Storage::disk($storage_disk)->url($path . '/' . $filename);
// Adjust URL format if necessary, ensuring correct public access links
// This part depends heavily on your S3 configuration setup.
return str_replace(config('filesystems.disks.s3.url'), config('filesystems.disks.s3.cloudfront_url'), $url);
}
return Storage::disk($storage_disk)->url($path . '/' . $filename);
}
Best Practices for File Handling in Laravel
When working with file operations, always favor the framework's abstractions. As you can see above, utilizing Storage::disk()->putStream() is superior to manually managing raw PHP file I/O when integrating with cloud storage. This approach adheres to the principles of clean separation and maintainability that Laravel promotes across its ecosystem, including the core design philosophy found at laravelcompany.com.
Key Takeaways:
- Stream First: Always treat external data as a stream when downloading large files from HTTP requests.
- Leverage Facades: Use Laravel's
Storagefacade to abstract away the complexities of S3, local storage, and disk configuration. - Error Handling: Always check if the stream was successfully opened before attempting to write data.
Conclusion
By switching from direct file reading (file_get_contents) to using PHP streams in conjunction with Laravel's Storage methods, we resolve the encoding issues and create a solution that is both secure and highly compatible with environment configurations. This pattern ensures that your application can reliably fetch external assets and store them efficiently in your chosen backend—be it local disk or S3. Happy coding!