Tech Verse Logo
Enable dark mode
Downloading Files from External URLs in Laravel

Downloading Files from External URLs in Laravel

Tech Verse Daily

Tech Verse Daily

4 min read

In this article, you’ll learn several practical and production‑ready ways to download files from external URLs in a Laravel application. We’ll cover how to:

  • Stream a remote file directly to the browser

  • Download and store a file on your server

  • Stream large files into storage without high memory usage

  • Serve stored files back to users

  • Combine remote fetching and user downloads safely

All examples use plain Laravel controllers, the HTTP client, and the Storage facade, so you can copy and adapt them directly into your own project.

Basic Controller Setup

All examples assume a simple controller called from routes.

Routes

// routes/web.php

use App\Http\Controllers\FileDownloadController;
use Illuminate\Support\Facades\Route;

Route::get('/download-remote', [FileDownloadController::class, 'downloadRemote']);
Route::get('/cache-remote', [FileDownloadController::class, 'cacheRemote']);
Route::get('/cache-remote-stream', [FileDownloadController::class, 'cacheRemoteStreamed']);
Route::get('/download-cached', [FileDownloadController::class, 'downloadCached']);
Route::get('/proxy-remote', [FileDownloadController::class, 'proxyRemote']);

Controller Skeleton

// app/Http/Controllers/FileDownloadController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use GuzzleHttp\Psr7\Utils;

class FileDownloadController extends Controller
{
   // Methods are shown below
}

Streaming a Remote URL Directly to the Browser

If you simply want to proxy a remote download through your Laravel app (for authentication, logging, or access control), you can stream the file without saving it to disk.

public function downloadRemote(Request $request)
{
    $url = $request->query('url');

    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $fileName = basename(parse_url($url, PHP_URL_PATH)) ?: 'downloaded-file';

    return response()->streamDownload(function () use ($url) {
        $handle = fopen($url, 'rb');


        if (! $handle) {
            abort(502, 'Unable to open remote file');
        }

        while (! feof($handle)) {
            echo fread($handle, 1024 * 1024); // 1 MB chunks
        }

        fclose($handle);
    }, $fileName);
}

Why this works well

  • Streams data in chunks (low memory usage)

  • Does not persist the file on disk

  • Ideal for authenticated or logged downloads

⚠️ Notes:

  • allow_url_fopen must be enabled in PHP

  • Slow remote servers will slow the response

Downloading a File and Storing It on Disk

For caching or reuse, it’s often better to download the file once and store it.

public function cacheRemote(Request $request)
{
    $url = $request->query('url');


    if (! $url) {
        abort(400, 'Missing url parameter');
    }

    $response = Http::timeout(30)->get($url);

    if (! $response->successful()) {
        abort(502, 'Failed to download remote file');
    }

    $extension = pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
    $fileName = 'remote_' . time() . ($extension ? ".{$extension}" : '');

    $path = "downloads/{$fileName}";

    Storage::disk('local')->put($path, $response->body());

    return response()->json([
        'stored_as' => $path,
        'disk' => 'local',
    ]);
}

Use this when

  • Files are small to medium sized

  • You want a quick, simple implementation

⚠️ This loads the entire file into memory once.

Streaming Large Files Directly into Storage (Best Practice)

For large files, stream the response directly to disk using Guzzle’s sink option.

public function cacheRemoteStreamed(Request $request)
{
    $url = $request->query('url');


    if (! $url) {
        abort(400, 'Missing url parameter');
    }


    $extension = pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
    $fileName = 'remote_' . time() . ($extension ? ".{$extension}" : '');


    $relativePath = "downloads/{$fileName}";
    $absolutePath = Storage::disk('local')->path($relativePath);


    $resource = Utils::tryFopen($absolutePath, 'w');


    Http::withOptions([
        'sink' => $resource,
        'timeout' => 60,
    ])->get($url);


    return response()->json([
        'stored_as' => $relativePath,
        'disk' => 'local',
    ]);
}

Advantages

  • Minimal memory usage

  • Safe for very large files

  • Production‑grade approach

Letting Users Download a Stored File

Once the file exists on disk, serving it is trivial.

public function downloadCached(Request $request)
{
    $path = $request->query('path');


    if (! $path) {
        abort(400, 'Missing path parameter');
    }


    if (! Storage::disk('local')->exists($path)) {
        abort(404, 'File not found');
    }


    return Storage::disk('local')->download($path);
}

You can also customize the filename and headers:

return Storage::disk('local')->download($path, 'report.pdf', ['Content-Type' => 'application/pdf']);

Proxying a Remote File to the User in One Request

If you want to fetch a remote file and immediately send it to the browser, without storing it:

public function proxyRemote(Request $request)
{
    $url = $request->query('url');


    if (! $url) {
        abort(400, 'Missing url parameter');
    }


    $fileName = basename(parse_url($url, PHP_URL_PATH)) ?: 'downloaded-file';


    $response = Http::timeout(60)->get($url);


    if (! $response->successful()) {
        abort(502, 'Failed to download remote file');
    }


    return response()->streamDownload(function () use ($response) {
        echo $response->body();
    }, $fileName, [
        'Content-Type' => $response->header('Content-Type', 'application/octet-stream'),
    ]);
}

✔ Good for small or moderate files ❌ Avoid for very large files

Downloading Files Using Queues or Cron Jobs

For slow or unreliable remote sources, avoid blocking HTTP requests.

Recommended flow:

  1. Controller dispatches a job with the remote URL

  2. Job downloads the file using cacheRemoteStreamed

  3. Job stores the file path in the database

  4. A separate endpoint exposes the file via Storage::download()

This approach improves reliability and user experience, especially for large files or third‑party APIs.

    Latest Posts

    View All

    Getting Started with Mago – The Fastest PHP Tooling Chain

    Getting Started with Mago – The Fastest PHP Tooling Chain

    Best Stack Recommendations for Laravel Projects (Battle-Tested in Production)

    Best Stack Recommendations for Laravel Projects (Battle-Tested in Production)

    Laravel + React Authentication the Right Way: Sanctum, JWT, or Passport?

    Laravel + React Authentication the Right Way: Sanctum, JWT, or Passport?

    Laravel PDF Generator: Spatie Laravel PDF vs Laravel DomPDF (In-Depth Comparison)

    Laravel PDF Generator: Spatie Laravel PDF vs Laravel DomPDF (In-Depth Comparison)

    how to systematically optimize Laravel databases in production

    how to systematically optimize Laravel databases in production

    Optimize Images in Laravel with Intervention Image

    Optimize Images in Laravel with Intervention Image

    Common Security Mistakes in Laravel Apps and How to Fix Them Properly

    Common Security Mistakes in Laravel Apps and How to Fix Them Properly

    Clean, Reusable Query Logic the Right Way: Laravel Global Scopes & Local Scopes

    Clean, Reusable Query Logic the Right Way: Laravel Global Scopes & Local Scopes

    Mastering Custom Blade Directives in Laravel

    Mastering Custom Blade Directives in Laravel

    Laravel 12.44: Adds HTTP Client afterResponse() Callbacks

    Laravel 12.44: Adds HTTP Client afterResponse() Callbacks