In Laravel, how do response()->download() and response()->file() differ when a concert-venue app serves a PDF ticket?
answer
- both wrap a file on local disk
- Content-Disposition: attachment vs none
- download($path, $name, $headers, $disposition)
- file($path, $headers) opens in the browser
- a Symfony BinaryFileResponse, not Illuminate Response
basics
~20 sBoth 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
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
Remember the one-line difference: download() makes the browser save the file under a name you choose, file() lets the browser display it.
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.
Authorize and resolve the path from a stored record, never from input, and know when to switch to disk-aware helpers or streamed downloads.
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.