HTTP requests and responses – Part 2

about a year ago by Miloslav Hůla     edit

In the first part I describe presenter methods, by which we can control HTTP responses of the application. In this section, I will address the interface Nette\Application\IResponse, with which HTTP responses can be formed very accurately.

Nette\Application\IResponse

The presenter method sendResponse() accepts a single argument. Object implementing interface Nette\Application\IResponse. Before we write our own implementation, let's look at the ready ones Nette\Application\Responses.

It is the most widely used, the most useful, but also the least known TextResponse. If the presenter takes an action and the HTML content is rendered from Latte, it is the one that is used. Let her stay calm in oblivion.

If you know Nette in more detail, you may have noticed that I did not mention the presenter method at all in the first part of this mini-series forward(). That's because it has nothing to do with HTTP. The method creates a class object ForwardResponse, looks like an HTTP response, sends it, but the application intercepts the response, does not send the HTTP redirection, and instead calls a new redirection target. The browser does not know anything, the URL does not change.

In contrast, class RedirectResponse is used by all redirect*() presenter methods. This is a redirect by HTTP protocol, the browser retrieves a new URL.

JsonResponse may seem more interesting, but it really only seems. This class is used by the presenter method sendJson() and thus I would conclude its description.

Really interesting is the class FileResponse. Sends a file to the browser and offers it for download. Imagine an app with user accounts. Lots of PDF invoices. But they must not be accessible to anyone! The access is controlled by the application, the directory structure is as follows:

app/
invoices/	<-- a lot of invoices in PDF, the browser can't get here
www/		 <-- document root

And now presenter, which allows downloading of invoices only to logged users.

use Nette\Application\Responses\FileResponse;

final class DownloadPresenter extends Nette\Application\UI\Presenter
{
	private $invoices;

	public function __construct(InvoiceRepository $invoices)
	{
		$this->invoices = $invoices;
	}

	public function startup(): void
	{
		parent::startup();
		if (!$this->getUser()->isLoggedIn()) {
			$this->redirect(':Admin:Sign:in');
			die();
		}
	}

	public function actionInvoice(int $id): void
	{
		try {
			$invoice = $this->invoices->get($id);
		} catch (InvoiceNotFoundException $e) {
			$this->error("An invoice with ID $id does not exist.");
			die();
		}

		$response = new FileResponse($invoice->file, "Invoice $invoice->number.pdf", 'application/pdf');
		$this->sendResponse($response);
	}
}

The presenter gets a repository in the constructor. In the method startup(), we will only grant access to logged-in users. And if an invoice exists, the action invoice sends it as a PDF file to the browser.

Class FileResponse does a lot of things for us. Properly formatted file name when using diacritics or interrupted download. It is not quite easy to solve it correctly. Also, the entire file is not read into memory but is sent in blocks. The necessity for large files.

Class FileResponse is irreplaceable. Nearly. Do you know the Apache module mod_xsendfile? If not, I will briefly introduce it.

Let's have a directory with files outside of the document root. Just as placed as the one with the invoices from the example above. The web browser does not access it. And yet you want to be able to download the files freely and, moreover, to log your accesses. You can use the same logic as in the example with PDF invoices and use FileResponse. But it has one drawback. If the files are large and clients access after a slow connection, it may take several hours to download the file. We may have accumulated hundreds of parallel downloads, for example. And for each connection runs one PHP process. If you have Apache and module mod_xsendfile, just send HTTP header X-SendFile and stop running the PHP application. The web server will take care of the file download. I will leave it to you to consider how useful it is. This fits me in the implementation example Nette\Application\IResponse. And maybe an anonymous class when we already have it in PHP 7. The following code snippet is a presenter action that handles the download.

public function actionDownload(string $path): void
{
	# $this->basePath  - for example /var/data/download
	# $this->logger	- fictional logging tool

	$file = $this->basePath . '/' . $path;
	if (!is_file($file)) {
		$this->error('File not found.');
		die();
	}

	# A little bit of security
	$file = realpath($file);
	if (strpos($file, $basePath . '/') !== 0) {
		$this->error(null, Nette\HTTP\IResponse::S403_FORBIDDEN);
		die();
	}

	# Implementation IResponse
	$response = new class($file) implements Nette\Application\IResponse
	{
		private $file;

		public function __construct(string $file)
		{
			$this->file = $file;
		}

		public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response)
		{
			$response->setHeader('X-SendFile', $this->file);
		}
	};

	$this->logger->notice("File download from $_SERVER[REMOTE_ADDR]: $path");
	$this->sendResponse($response);
}

I think nothing complicated. Sending a response only sets the HTTP header. Everything else is solved by Apache.

Nginx has a similar functionality. The control is passed through the header X-Accel-Redirect and its meaning is slightly different. You are not sending an absolute path in the file system, but a URI whose mapping to a specific directory is set in the configuration.

Using an anonymous class is cool, but the code is not reusable. Let's write a reusable class, response for sending CSV, a popular spreadsheet data export.

final class CsvResponse implements Nette\Application\IResponse
{
	private $fileName;
	private $rows;
	private $delimiter;

	public function __construct(string $fileName, iterable $rows, string $delimiter = ',')
	{
		$this->fileName = $fileName;
		$this->rows = $rows;
		$this->delimiter = $delimiter;
	}

	public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response)
	{
		$response->setContentType('text/csv', 'utf-8');
		$response->setHeader('Content-Description', 'File Transfer');

		# Trochu vykrademe Nette\Application\Responses\FileResponse
		$tmp = str_replace('"', "'", $this->fileName);
		$response->setHeader(
			'Content-Disposition',
			"attachment; filename=\"$tmp\"; filename*=utf-8''" . rawurlencode($this->fileName)
		);

		$bom = true;
		$fd = fopen('php://output', 'wb');

		foreach ($this->rows as $row) {
			if ($bom) {
				# In order for MS Excel to display diacritics correctly. But only if there are any lines
				fputs($fd, "\xEF\xBB\xBF");
				$bom = false;
			}

			$row = $row instanceof \Traversable ? iterator_to_array($row) : (array) $row;
			fputcsv($fd, $row, $this->delimiter);
		}

		fclose($fd);
	}
}

I will not comment on the code, I believe it is understandable. Use is easy. Create an instance of the class and pass it to the presenter method sendResponse().

The latest implementation Nette\Application\IResponse is CallbackResponse. It is a universal class and requires a callback in the constructor to be called in send() method. All of the examples above can be overridden with CallbackResponse. The anonymous class a bit lost in charm, but some very lazy answers are still useful.

Whether you find these implementations useful or not, I strongly recommend looking at their code before you write your own implementations. There is something to inspire and it gets better under your skin.

This summarized almost everything I wanted to write about HTTP in the Nette application. The third part will be just such a cherry.