skip to content

In Laravel, how do response()->download() and response()->file() differ when a concert-venue app serves a PDF ticket?

level: middleimportance: should knowfreq 40%

answer

  1. both wrap a file on local disk
  2. Content-Disposition: attachment vs none
  3. download($path, $name, $headers, $disposition)
  4. file($path, $headers) opens in the browser
  5. a Symfony BinaryFileResponse, not Illuminate Response

basics

~20 s

Both return a Symfony BinaryFileResponse for a file on the server's disk. download() adds Content-Disposition: attachment with a chosen filename, so the browser saves it; file() adds no disposition, so the browser displays the PDF inline.

solid answer

~40 s

`response()->download($path, $name = null, $headers = [], $disposition = 'attachment')` builds a `BinaryFileResponse` and sets `Content-Disposition: attachment; filename=...`, so the browser saves the ticket under the name you choose rather than the file's name on disk. Laravel also generates an ASCII fallback name with `Str::ascii()` for clients that cannot read the UTF-8 one. `response()->file($path, $headers = [])` builds the same response class without a disposition, so a browser with a PDF viewer shows the ticket in the tab. Both need a local path or `SplFileInfo`; a file on a Storage disk such as S3 is served through the Storage API instead. Passing `'inline'` as the fourth argument of `download()` gives inline display with a suggested filename. The result is a Symfony response, so extra headers go in the headers argument.

code

php · 21 lines
php
<?php

use App\Models\Ticket;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class TicketPdfController
{
    public function download(Ticket $ticket): BinaryFileResponse
    {
        return response()->download(
            storage_path('app/private/'.$ticket->pdf_path),
            "ticket-{$ticket->row}{$ticket->seat}.pdf",
            ['Content-Type' => 'application/pdf'],
        );
    }

    public function show(Ticket $ticket): BinaryFileResponse
    {
        return response()->file(storage_path('app/private/'.$ticket->pdf_path));
    }
}

go deeper

for a junior

Remember the one-line difference: download() makes the browser save the file under a name you choose, file() lets the browser display it.

for a middle

Explain the Content-Disposition header each sets, the download() argument order including the disposition, and that both return a Symfony BinaryFileResponse for a local path.

for a senior

Authorize and resolve the path from a stored record, never from input, and know when to switch to disk-aware helpers or streamed downloads.

for a principal

Decide whether files are served by the app, by signed storage URLs or by the web server, weighing worker time against access control and audit needs.

## The situation A concert-venue app stores each generated ticket as a PDF under `storage/app/private/tickets/`. Two screens use it: the order page offers a **Download ticket** button, and the ticket page shows the PDF in the browser so the buyer can check the seat before printing. Laravel's response factory has one method for each. ## What the two methods build Both methods live on `Illuminate\Routing\ResponseFactory` and both return a Symfony `BinaryFileResponse`. That class is built for sending an existing file from disk: it takes the path and sends the file's bytes when the response is sent, so the controller never reads the PDF into a PHP string. | | `download()` | `file()` | |---|---|---| | Signature | `download($file, $name = null, array $headers = [], $disposition = 'attachment')` | `file($file, array $headers = [])` | | `Content-Disposition` | `attachment; filename="..."` (or `inline` if you pass it) | not set | | Browser behaviour | save dialog or downloads folder | display in the tab if it can (PDF, image) | | Filename the user sees | `$name`, or the file's own basename | chosen by the browser, usually from the URL | | Returns | `BinaryFileResponse` | `BinaryFileResponse` | ## Details of `download()` - **The second argument is the filename**, not a status code. `download($path, 'ticket-row-F-seat-12.pdf')` saves under that name even though the stored file is `9f2c...pdf`. - **Non-ASCII names get a fallback.** When you pass a name, Laravel calls `setContentDisposition($disposition, $name, $fallback)` with a fallback built by `Str::ascii()` and stripped of `%`. A name like `Billet-Théâtre.pdf` is sent with an ASCII version (`Billet-Theatre.pdf`) for clients that cannot read the UTF-8 form. The docs still warn that Symfony needs an ASCII filename; the pinned source shows Laravel supplying that fallback itself. - **The fourth argument is the disposition.** `download($path, $name, [], 'inline')` asks the browser to display the file while still suggesting a name for when the user saves it. - **Headers go in the third argument**, for example `['Content-Type' => 'application/pdf']` if you want to state the type rather than rely on detection. ## Details of `file()` `file()` passes only the path and headers to `BinaryFileResponse`. With no `Content-Disposition` header, the browser treats the response like any other resource: it renders PDFs and images in place and falls back to a download for types it cannot display. It is the right tool for an image proxy or a "view your ticket" page. ## Things that trip people up 1. **Local files only.** Both methods take a filesystem path or an `SplFileInfo`. A ticket on an S3 disk has no local path; the Storage facade has its own `download()` and response helpers for disks, and those belong to the filesystem layer. 2. **They are Symfony responses.** `BinaryFileResponse` does not use Laravel's `ResponseTrait`, so the fluent helpers of `Illuminate\Http\Response` are not all available on it. Put headers in the headers argument or on `$response->headers`. 3. **Never build the path from raw input.** `download(storage_path('app/private/tickets/'.$request->input('f')))` lets a crafted value walk out of the directory. Look the ticket up by id and authorize it first, then use the path stored on the record. 4. **Content that does not exist yet is a different tool.** A PDF rendered on the fly, or a CSV built row by row, has no file to point at; that is what streamed downloads are for. ## A decision guide for the venue app - **Download button on the order page:** `download()` with a friendly name such as `ticket-F12.pdf`. - **Preview on the ticket page:** `file()`, or `download()` with `'inline'` when you also want a sensible Save As name. - **Seat-map image served through the app for access control:** `file()` with a `Content-Type` header. - **Ticket stored on a cloud disk:** the Storage facade's helpers or a temporary URL, not these two methods. - **Ticket rendered on the fly and never saved:** a streamed download. Whichever you pick, resolve the path from a stored record after authorization, so the response method only ever sees a trusted path. ## What interviewers want to hear The core is one header: `download()` sets `Content-Disposition: attachment` with a chosen name, `file()` sets none. Strong answers add the argument order, the inline option, the local-path requirement and the fact that both return a `BinaryFileResponse`.

  • How do you show the PDF inline but still suggest a filename if the user saves it?
    Call `response()->download($path, 'ticket.pdf', [], 'inline')`. The fourth argument sets the disposition type, so the header becomes `inline` with the filename, and the browser displays the file while using that name for Save As.
  • The tickets move to an S3 disk. Why does `response()->download()` stop being usable?
    `download()` and `file()` build a `BinaryFileResponse`, which needs a file on the local filesystem. An S3 object has no local path, so you serve it through the Storage facade's disk-aware helpers or a temporary URL instead.

saying these in an interview costs you the question

  • response()->file() forces the browser to save the file.
  • The second argument of download() is the HTTP status code.
  • download() accepts a path on any Storage disk, including S3.
  • download() and file() return an Illuminate\Http\Response with all its helpers.
  • A non-ASCII download name always breaks the response.