Bonnes pratiques sur les codes retours et la gestion des erreurs lorsqu’on écrit une API.

Symfony est bien conçu, lorsqu’un client demande une route qui n’existe pas, 404 Not Found est renvoyé. Une mauvaise méthode et c’est 405 Method Not Allowed qui part.

Renvoyer le bon code HTTP est pratique, surtout lorsqu’on écrit des API (REST - mais pas que). C’est une façon assez standardisé d’indiquer au client le comportement à adopter.

Imaginons que l’API propose de réaliser une action longue comme envoyer un email ou compresser une archive zip. Il est préférable que cette action soit asynchrone pour ne bloquer ni le serveur ni le client.

Renvoyer le code retour habituel 200 OK, n’est pas très approprié :

Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action.

L’action n’étant pas terminée, on ne peut renvoyer son statut dans le corps de la réponse.

Il faut donc utiliser 202 Accepted :

The request has been accepted for processing, but the processing has not been completed. The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place.

De la même manière, on peut renvoyer 204 No Content lorsqu’on a rien à mettre dans le corps de la réponse :

The server successfully processed the request, but is not returning any content. Usually used as a response to a successful delete request.

Dans Symfony

Dans un contrôleur, il suffit d’utiliser le second paramètre de l’objet Response.

<?php

	use Symfony\Component\HttpFoundation\Request;
 	use Symfony\Component\HttpFoundation\Response;

	//...

		public function someAction(Request $request) {

			//déclencher une action asynchrone
			doSomethingLater();

			//répondre tout de suite
			return new Response("Accepted", 202);
		}

Passé Sf 2.4, on peut même utiliser des constantes pour améliorer encore la lisibilité :

<?php
   //...
	return new Response("Accepted", Response::HTTP_ACCEPTED);

Cette expressivité est encore plus nécessaire lorsqu’il s’agit de traiter les erreurs.

Cela permet par exemple, lorsque le client demande une ressource, de faire la différence entre un utilisateur non connecté et un utilisateur connecté mais qui n’a pas de droits d’accès suffisants.

  • Renvoyer 401 Unauthorized (pour le premier cas), va indiquer au client qu’il doit se connecter pour continuer.
  • Tandis que 403 Forbidden indique que l’accès à la ressource est interdite au client (et donc qu’il n’a pas les droits).

La bonne nouvelle, c’est que le Firewall de symfony s’en charge déjà à notre place.

Coté client

Coté client, il est bien plus facile de ne se baser que sur le code retour de la réponse plutôt que de s’inventer un protocole dans le corps de la réponse (qui sera donc propre à l’appliciation, jamais spécifié nulle part, bref une dette technique de plus pour l’avenir).

Par exemple, avec un client AngularJS, une bonne façon de traiter ce cas:

$http({method: 'GET', url: '/someUrl'}).
    success(function(data, status, headers, config) {
    	//c'est bon on a les données
    }).
    error(function(data, status, headers, config) {
      if (status == 401)
      	showLoginForm();
      else if (status == 403)
      	showAccessDeniedErrror();
    });

Exceptions (symfony2)

En ce qui concerne les erreurs, il y a même mieux que juste retourner une réponse : déclencher une exception.

La raison est simple; en plus de retourner le bon code retour HTTP, des traces sont écrites dans les logs.

Ainsi, au lieu d’écrire

<?php
 //...

 $this->get('logger')->error('415 Unsupported Media Type');
 return new Response('415 Unsupported Media Type', Response::HTTP_UNSUPPORTED_MEDIA_TYPE);

Il suffira de faire :

<?php
 use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; 
 //...

 throw new UnsupportedMediaTypeHttpException('415 Unsupported Media Type');

Il en existe de toutes sortes dans HttpKernel/Exception.

Pointeurs