HTTP požadavky a odpovědi – Část 1.

před 5 lety od Miloslav Hůla  

Nette nabízí pro práci s HTTP dvě vrstvy abstrakce. První, nízkoúrovňovou, obstarávají třídy ze jmenného prostoru Nette\Http. Nabízejí příjemné API pro práci s HTTP hlavičkami, vyžádanou URL, příchozími parametry a soubory, nebo nastavení číselného kódu odpovědi. Od této vrtvy jsme v Nette aplikaci téměř odstíněni.

Druhá vrstva svým způsobem zapouzdřuje tu první. Třídy se nacházejí ve jmenném prostoru Nette\Application a využijí se, pokud pracujete s Nette aplikací (a teď myslím doslovně Nette\Application\Application) a jejími presentery a akcemi. Objekt třídy Nette\Application\Request je výsledkem práce URL routeru. Router (Nette\Aplication\IRouter) vezme HTTP požadavek a podle definovaných rout ho přetaví na Nette\Application\Request. Ten nese informaci, jaký :modul:presenter:akce se má vykonat. Dále moc zajímavý není. Daleko zajímavější je Nette\Application\IResponse. Zejména té se bude věnovat tato krátká série třech článků.

HTTP odpovědi z aplikace

Než se k manipulaci s Nette\Application\IResponse dostaneme, shrnu nástroje pro ovládání HTTP protokolu, které máme při běhu presenteru k dispozici. Potomci třídy Nette\Application\UI\Presenter, pravděpodobně tedy všechny naše presentery, jich mají k dispozici opravdu hodně.

Metodu redirect() určitě znáte z tutoriálů. Co ale následující kód přesně udělá z pohledu HTTP protokolu?

public function actionDefault(): void
{
	$this->redirect(':Front:Homepage:help');
}

V konečném výsledku nastaví kód HTTP odpovědi na 302 (nebo 303), nastaví HTTP hlavičku Location na URL, kterou vytvoří podle vzoru :Front:Homepage:help, ukončí běh presenteru a pošle HTTP odpověď klientovi. Klient, například webový prohlížeč, si odpověď přečte a načte novou stránku z URL, kterou dostal. Pro nás velmi pohodlné.

Dále tu máme metodu redirectPermanent(), která funguje stejně, ale jako kód odpovědi použije 301, tedy trvalé přesměrování. Vybrat ten správný kód je někdy věda.

A do třetice, metoda redirectUrl(). Metodě předáváme URL a volitelně kód přesměrování. Například:

public function actionDefault(): void
{
	$this->redirectUrl('https://my.new.web/');
}

Další pomocník, metoda error(). Vše se zase točí kolem návratového kódu HTTP odpovědi. Metoda má jako výchozí 404, tedy „stránka nebyla nalezena“. Kdy metodu error() použít? Záleží na vás. Dejme tomu, že máme blog a někdo si chce přečíst neexistující článek https://example.com/read/123456.

public function actionRead(int $id): void
{
	try {
		$article = $this->articles->get($id);
	} catch (ArticleNotFoundException $e) {
		# Článek neexistuje, co teď?

		# Můžeme uživatele upozornit a přesměrovat na seznam článků.
		$this->flashMessage("Sorry, article with ID '$id' does not exist. Try another one.", 'warning');
		$this->redirect('Blog:list');

		# Anebo použít HTTP kód 404 - Not Found
		$this->error("Article with ID '$id' was not found.");
	}
}

Největší rozdíl je v tom, co se zobrazí uživateli. V prvním případě seznam existujících článků, v případě volání error() se zobrazí ErrorPresenter a chybu 404 navíc zaloguje webserver. To se může hodit. Volání error() mi přijde korektnější, lépe vystihuje situaci.

EDIT: Jakub Vrána v komentářích připomněl, že přesměrování z neexistujícího článku je antipattern. Znepříjemní to ruční zadávání URL při překlepu a chybné odkazy vyhledávače zaindexují jako seznam článků. Jednoznačné argumenty pro použití 404.

Jako druhý parametr metody error() můžeme uvést číslený kód chyby. Chybových HTTP kódů jsou mraky. Tak například „forbidden“:

public function actionRead(int $id): void
{
	try {
		$article = $this->articles->get($id);
	} catch (PermissionDeniedException $e) {
		$this->error('You Shall Not Pass!', Nette\HTTP\IResponse::S403_FORBIDDEN);
	}
}

Další metoda presenteru slouží pro odeslání dat ve formátu JSON. Jmenuje se sendJson() a použití nemůže být intuitivnější:

public function actionGetUserInfo(string $username): void
{
	$this->sendJson([
		'username' => ...,
		'firstName' => ...,
		'lastName' => ...,
	]);
}

Nemusíme se o nic starat. Pole se správně převede do JSON formátu, nastaví se správné HTTP hlavičky Content-Type a Content-Length, klient bude spokojený.

Další dvě metody presenteru, getHttpRequest() a getHttpResponse(). Názvy trochu napoví. Vracejí objekty Nette\Http\IRequest a Nette\Http\IResponse, ono nízkoúrovňové HTTP API, o kterém jsem se zmínil v úvodu. Díky všem těm metodám, které jsem výše popsal, je skoro nepotřebujeme. Ale jeden zjednodušený příklad bych měl.

Představte si, že náš blog, naše Nette aplikace, má poskytnout jeden REST API endpoint. Bude vracet jméno autora, nadpis článku a čas publikování. Nebude ale pro každého, jen pro klienty s tajuplným tokenem.

public function actionArticleData(int $id): void
{
	$request = $this->getHttpRequest();
	if ($request->getHeader('Token') !== 'eb0f21d63594e58d6b9995a7d2ac156c') {
		$this->error('Invalid or missing access token.', Nette\HTTP\IResponse::S403_FORBIDDEN);
		die();
	}

	try {
		$article = $this->articles->get($id);
	} catch (ArticleNotFoundException $e) {
		$this->error('Article not found.');
		die();
	}

	$this->sendJson([
		'id' => $id,
		'author' => $article->author->fullName,
		'title' => $article->title,
		'publishedAt' => $article->publishedAt->format(\DateTime::ISO8601),
	]);
}

Asi jste si všimli volání die() po error(). Není nutné. Všechny metody o kterých jsem dosud psal ukončí zpracování a die() se ani nezavolá. Používám to, protože při prvním pohledu do kódu metody vidím, kde její běh může skončit.

A to je vše. Zbývá už jen metoda sendResponse() a na tu se podíváme v dalším dílu.

Komentáře (RSS)

  1. Přesměrování při nenalezení článku je anti-pattern. Co když píšu URL ručně a udělám překlep? Přesměrování mi ho nedovolí upravit. Ale je s tím i mnoho dalších problémů, např. se neplatné odkazy zaindexují jako odkaz na seznam článků.

    Volání die() je anti-pattern kvůli testům.

    před 5 lety
  2. Díky za doplnění, souhlasím s oběma body.

    před 5 lety
  3. Moc pěkné shrnutí.

    před 5 lety

Chcete-li odeslat komentář, přihlaste se