Une API REST doit-elle renvoyer une erreur 500 Internal Server pour indiquer qu'une requête fait référence à un objet inexistant?


39

Je travaille avec une API REST qui réside sur un serveur qui gère les données pour une multitude de périphériques IoT.

Ma tâche consiste à interroger le serveur à l'aide de l'API pour collecter des informations de performances spécifiques concernant ces périphériques.

Dans un cas, j'obtiens une liste des périphériques disponibles et leurs identifiants correspondants, puis interroge le serveur pour plus de détails à l'aide de ces identifiants (GUID).

Le serveur renvoie un 500 Internal Server Errorpour une requête sur l'un de ces ID. Dans mon application, une exception est levée et je ne vois pas les détails de l'erreur. Si j'examine la réponse de plus près avec Postman , je peux voir que le serveur a renvoyé JSON dans le corps qui contient:

errorMessage: "This ID does not exist".

Ne tenez pas compte du fait que le serveur a d'abord fourni l'ID - il s'agit d'un problème distinct pour le développeur.

Une API REST doit-elle renvoyer a 500 Internal Server Errorpour signaler qu'une requête fait référence à un objet qui n'existe pas? À mon avis, les codes de réponse HTTP doivent faire référence strictement au statut de l'appel REST, plutôt qu'aux mécanismes internes de l'API. Je m'attendrais à un 200 OKavec la réponse contenant l'erreur et la description, qui serait la propriété de l'API en question.


Il me semble qu’il existe une différence potentielle dans les attentes en fonction de la structure de l’appel REST.

Considérez ces exemples:

  1. http://example.com/restapi/deviceinfo?id=123
  2. http://example.com/restapi/device/123/info

Dans le premier cas, l'ID de périphérique est transmis en tant que variable GET. Un 404 ou un 500 indiquerait que le chemin ( /restapi/deviceinfo) est introuvable ou a entraîné une erreur de serveur.

Dans le second cas, l'ID de périphérique fait partie de l'URL. Je comprendrais mieux un 404 Not Found, mais je pourrais quand même argumenter en fonction des parties du chemin qui sont interprétées comme des variables par rapport aux points d'extrémité.


Cette condition que vous décrivez est-elle considérée comme un succès ou un échec?
Robert Harvey


@RobertHarvey Je m'attendais à ce que l'API renvoie des informations sur le périphérique. La requête elle-même aurait dû être un succès, mais l'ID de périphérique ayant disparu ne devrait pas provoquer d'échec au niveau de la requête.
JYelton

18
Un identifiant qui n'existe pas me semble un 404. La raison la plus courante des erreurs 500 est que les programmeurs permettent à une exception interne de se décomposer afin que le serveur d'hébergement puisse la gérer. Parfois, il y a des cas vraiment exceptionnels où cela se produit et parfois, ce n'est que de la programmation paresseuse. Difficile de dire ce qui se passe ici.
Berin Loritsch

9
500 serveur interne signifie "C'est de notre faute, nous avons foiré quelque chose" et vous ne devez jamais chercher à renvoyer cet état à un utilisateur. Son but est essentiellement d'indiquer un bogue - votre utilisateur peut dire qu'il reçoit un 500 lorsqu'il fait une demande particulière, puis vous pouvez y remédier et le corriger.
Berry120

Réponses:


96

Je pense qu'une réponse 404 est la meilleure correspondance sémantique ici, car la ressource que vous tentiez de trouver (telle que représentée par l'URI utilisé pour la requête) n'a pas été trouvée. Renvoyer une charge d'erreur dans le corps est raisonnable, mais pas obligatoire.

Selon la RFC 2616 , la définition du code d'état 404 est la suivante:

10.4.5 404 non trouvé
Le serveur n'a rien trouvé qui corresponde à l'URI de demande. Aucune indication n'est donnée quant à savoir si la condition est temporaire ou permanente. Le code d'état 410 (Gone) DEVRAIT être utilisé si le serveur sait, par le biais d'un mécanisme configurable en interne, qu'une ancienne ressource est indisponible en permanence et ne possède pas d'adresse de transfert. Ce code d'état est couramment utilisé lorsque le serveur ne souhaite pas révéler exactement pourquoi la demande a été refusée ou lorsqu'aucune autre réponse n'est applicable.


6
404 n'est une correspondance sémantique que si l'identifiant qui n'existe pas correspond à la ressource en cours de récupération.
Robert Harvey

55
@ThomasOwens Une requête ne renvoyant aucun résultat renverrait 200 statut et un tableau de résultats vide. Un 404 ne serait renvoyé que si vous spécifiez un ID d'objet spécifique qui n'existe pas réellement.
Sean Burton

11
@ThomasOwens: du point de vue de l'appelant, le point final n'est pas /questions, c'est /questions/368213. Ce point final n'existe pas dans votre scénario. Pensez-y de cette façon: si vous faites un GET sur / foo / bar et qu'il n'y a pas de barre, pourquoi la réponse devrait-elle être différente si / foo existe ou non?
Bryan Oakley

17
D'accord, ce n'est pas une erreur de serveur, nous n'envoyons donc pas de 5xx. C'est une erreur du client - le client a demandé quelque chose qui ne lui est pas destiné, nous envoyons donc 4xx, plus précisément le 404.
bdsl

7
@ThomasOwens: How do you differentiate between a 404 meaning "the query returned no results" and a 404 meaning "the endpoint does not exist"?- 400 Bad Request.
Robert Harvey

34

Je vais utiliser vos exemples.

http://example.com/restapi/deviceinfo?id=123

Si le noeud final renvoie un tableau JSON , le meilleur choix est 200 OKd'utiliser un tableau vide si aucun résultat n'a été trouvé.

Si le critère d' évaluation est conçu pour renvoyer un seul résultat , mon choix serait 404 NOT FOUND, parce que, pour moi, la bonne syntaxe pour ce type de point final est: http://example.com/restapi/deviceinfo/123. J'utilise généralement request param uniquement pour le filtrage et lorsque mon noeud final renvoie un tableau.

http://example.com/restapi/device/123/info

Je pense que cette question a déjà été répondu ici . POST ou GET, le meilleur choix semble être 404 NOT FOUNDparce que la ressource 123n’a pas été trouvée.

Dans les deux cas, je ne vois pas la nécessité d'expliquer pourquoi la demande n'a pas été complétée. Les informations de requête et le code HTTP expliquent déjà pourquoi.


2
Comment faites-vous la distinction entre un identifiant inexistant et une mauvaise URL?
Robert Harvey

2
@luizfzs, vous le pouvez aussi, je ne vois pas de problème à ce sujet. Dans certaines API que j'ai développées, je préfère utiliser 204 dans une opération PUT ou DELETE réussie ( comme expliqué ici ) sans corps de réponse.
Dherik

10
Pourquoi devriez- vous distinguer entre un identifiant inexistant et une mauvaise URL, à ce niveau? De toute façon, vous faites toujours une demande valide, en ce qui concerne HTTP. Le serveur ne ... trouve pas la ressource à laquelle vous vous adressez. Une mauvaise URL n'est pas une requête malformée. c'est une demande bien faite pour la mauvaise chose .
cHao

6
@cHao Parce qu'en tant que développeur, je veux savoir si mon application échoue parce que l'élément n'existe pas ou si j'ai accidentellement pointé mon application à app.company.cxm / test / plutôt qu'à app.company.cxm / tst /
Patrick M

7
@PatrickM: En tant que développeur, vous devez savoir que les réponses d'erreur peuvent également contenir un corps de message. C'est là que les informations d'erreur explicatives vont, si vous le voulez vraiment. Ne détruisez pas la sémantique simplement parce que vous êtes paranoïaque à propos des gros doigts. Au niveau HTTP, peu importe que vous fassiez comme /itms/1234ou /items/12234.
cHao

27

HTTP 404 est correct, car le serveur comprend la ressource demandée par le client, mais ne dispose pas de cette ressource.

Le fait que vous travailliez avec une "API REST" est la clé. L’API doit se comporter comme si elle effectuait un transfert d’état représentatif, et non une fonction. (Bien sûr, le terme "REST" a pris un sens plus large, mais vous pouvez toujours utiliser ici son sens littéral.) Le client a demandé l’état de la ressource décrite par l’URL http://example.com/restapi/device/123/info. Une chaîne de requête ( /deviceinfo?id=123) ne changerait pas la situation. Le serveur sait que vous demandez le transfert de l'état du périphérique 123, mais il ne le reconnaît pas comme une ressource connue. Donc HTTP 404.

Les autres réponses possibles discutées ici ont aussi des significations spécifiques:

  • HTTP 200- Nous avons l'état pour vous. c'est dans le corps de la réponse.
  • HTTP 204- Nous avons l'état pour vous. c'est vide.
  • HTTP 400- Nous ne pouvons pas dire de quelle ressource vous parlez. Fixez votre URL.
  • HTTP 500- Nous avons mal fonctionné. Pas ta faute.

Voir RFC 2616 Sec. 10 selon le cas.


Je suis d'accord avec votre point de vue. Si le serveur retournait un 404, cela aurait plus de sens que ce qu'il fait (un 500). Merci.
JYelton

11

Une erreur 5xx est généralement utilisée pour indiquer que le serveur a rencontré une erreur et ne peut pas terminer la demande. Si le serveur accepte la demande, peut l'analyser avec succès et fait ensuite son travail, cela ne devrait pas renvoyer d'erreur 5xx.

Je ne sais pas s'il existe une quelconque convention sur ce qu'il faut retourner si une requête ne donne aucun résultat. J'ai vu à la fois ce que vous décrivez (un 200 avec un corps contenant un message) et un 404 indiquant que les résultats n'ont pas été trouvés. Le 200 est probablement le plus logique - la demande a abouti et il n'y a eu aucun problème avec la demande du client ou pendant le traitement de la demande par le serveur. Un corps peut transmettre un message au client.


Je traiterais vos deux exemples ( http://example.com/restapi/deviceinfo?id=123et http://example.com/restapi/device/123/info) de la même façon, 123c’est un paramètre. Les deux cas sont des manières différentes de structurer une demande pour obtenir les informations de périphérique pour un périphérique avec l'ID 123.

Premièrement, je considérerais l'autorisation et l'authentification. Si l'utilisateur ne dispose pas des autorisations appropriées, je renverrais un 403 ou un 401 selon le cas. Bien que cela s'appelle "Unauthorized", je crois comprendre que 401 concerne davantage l'authentification et 403, une autorisation non autorisée ou une autorisation refusée. Je ne serais cependant pas trop pointilleux ici, si vous vouliez simplement vous en tenir à 403 pour toutes les erreurs d'authentification et d'autorisation.

Ensuite, je gérerais l'ID. D'après votre exemple, cela ressemble à un identifiant numérique. Si une valeur non numérique était fournie, je renverrais un 400. Si le paramètre pouvait éventuellement être un identifiant de périphérique valide, je poursuivrais le traitement. S'il y avait d'autres arguments, ils seraient également vérifiés ici. Je m'attendrais à ce que le corps de la réponse contienne les informations appropriées sur les raisons pour lesquelles la demande était incorrecte.

Si tous les paramètres étaient valides, je commencerais à traiter la demande. Si le système ou une dépendance quelconque (base de données, service tiers, autre service interne) n'est pas disponible, je renverrais un code 5xx - 503 serait spécifique, mais un code 500 serait également acceptable. Dans les deux cas, je retournerais un corps avec des détails supplémentaires. Ne considérez pas que si une dépendance externe signale un délai d'expiration de la demande 408, je la consommerais et renverrais 500 à mon client, ce qui permettrait à un client de recevoir un 408 uniquement si la demande de mon système expirait. Si le système est en mesure de compléter la demande, je renverrais un 200 et l'organisme approprié.

204 peut être utile dans certains cas, mais cela vous empêche d'envoyer un corps de réponse. En particulier dans un paramètre d'API, envoyer un corps de réponse avec des informations pouvant être introduites dans un mécanisme de journalisation ou de rapport semble être la bonne décision à prendre dans la plupart des cas.

Si un serveur 404 est renvoyé, c'est uniquement si le serveur ne dispose pas d'un /deviceinfoterminal ou d'un /device/:id/infoterminal.

Je ne considérerais pas qu'un identifiant qui ne soit pas identique à la ressource non trouvée. La ressource est l'information sur le périphérique pour un périphérique particulier (dans votre exemple). Le renvoi d'un 404 signifierait que la ressource (les informations sur le périphérique) n'existe pas. Un 200 avec un corps approprié signifie que le système peut en effet fournir des informations sur le périphérique. Il peut y avoir ou non un périphérique avec l'ID spécifié.


1
@ RobertHarvey Oui. Il n'y avait pas d'erreur. Le fait que le serveur ait généré le GUID et l’a envoyé au client ne signifie pas qu’une autre opération l’a supprimé. La demande / réponse a abouti. La commande ou la requête représentée par la demande n'a pas renvoyé de données. Pour moi, ce n'est pas un échec. Cela pourrait être une exception, mais je ne suis pas convaincu que cela mérite un 5xx.
Thomas Owens

2
Si je concevais cette API que j'interroge, je l'aurais renvoyé 200 avec un corps contenant ensuite quelque chose à l'effet device: 123; status: not found;. Ensuite, je sais que la requête a fonctionné et que l’API me fournit des informations utiles.
JYelton

7
@Blrfl Cela est soutenu (bien que pas "prouvé") par le libellé de nombreux sites Web de 500 pages, généralement quelque chose comme "Nous avons commis une erreur et allons enquêter". Dans mon esprit, un serveur qui fonctionne correctement n'envoie jamais des 500, sauf peut-être dans le cas des rayons cosmiques.
Mbrig

6
Http n'a pas de concept de "point final", pas de distinction pertinente entre une variable et le reste de l'URL, et donc ne reste pas non plus. Vous pouvez avoir un système de routage qui correspond à une expression rationnelle et qui achemine des milliers d'URL vers une fonction de contrôleur, mais il s'agit d'un détail de la mise en œuvre, pas d'une partie fondamentale de l'API. Une réponse 200 sur un get est uniquement pour le cas où la ressource demandée est en cours d'extraction. Dans ce cas, le client souhaite une représentation d'une ressource qui n'existe pas. La réponse correcte est 404.
bdsl

8
Un système fonctionnant correctement n'envoie jamais une réponse 500. Un serveur fonctionnant correctement peut envoyer un 500 parce que s'il fait partie d'un système plus grand et qu'il a détecté une erreur interne à ce système, par exemple, la base de données est en panne ou un autre service dépendant du service Web renvoie des résultats sans signification.
bdsl

5

Une erreur HTTP de série 500 indique un dysfonctionnement du serveur. Mis à part 501 Not Implementedet 505 HTTP Version Not Supported, l'utilisation de ces codes d'erreur implique que la nouvelle tentative ultérieure de la demande peut aboutir (bien que 503 Service Unavailablene l'indique explicitement que cela). Dans l’idéal, un serveur ne devrait jamais produire l’un de ces codes, mais son incapacité à écrire un logiciel exempt de bogues et à le doter de ressources infinies signifie que vous en aurez besoin de temps à autre.

Pour un résultat "objet inexistant", vous devez probablement renvoyer soit 404 Not Found(lorsque la demande concerne un objet par nom), soit 200 Successavec un corps de résultat vide (lors de la recherche d'objet par attributs). 204 No ContentCela semble tentant, mais je l’utiliserais seulement dans les situations où l’absence d’un corps de réponse est le résultat attendu.


1

En tant que client de votre API, lorsque je passe l'un de ces appels:

  1. http://example.com/restapi/deviceinfo?id=123
  2. http://example.com/restapi/device/123/info

Je m'attends à récupérer une (représentation) d'un DeviceInfoobjet (ou un type particulier, de toute façon, qu'il s'agisse d'un type formel ou simplement conforme à une convention documentée "type de canard"). Je veux un 200statut qui signifie que j'en ai un, et que je peux l'utiliser et l'utiliser.

Pour les API REST, je considère les codes d’état 400 et 500 comme des exceptions. Vous les utilisez pour indiquer lorsque vous ne pouvez pas renvoyer une réponse "normale" à la demande que vous avez reçue. Le client devra donc faire quelque chose d'exceptionnel plutôt que de traiter les informations qu'il s'attendait à récupérer.

Cela signifie, en tant que consommateur d’API, que je peux utiliser une sorte de fonction d’appel vérifié-en attente qui extrait une réponse ou lève une exception. C'est génial; ma logique normale peut être un code en ligne droite, et je peux organiser ma gestion des erreurs de la même manière que dans le code local. Les 404 inattendus se manifesteront sous forme d'exceptions "aucune ressource trouvée" sans que je doive faire quoi que ce soit, et non sous forme d'erreur "d'attribut manquant" lorsque je traiterai ultérieurement { errorMessage: "Device 123 not found" }comme s'il s'agissait d'un DeviceInfoobjet.

Si vous estimez que le noeud final http://example.com/restapi/deviceinfo est trouvé et que seul le résultat id=123n'est pas trouvé et que 200 est renvoyé avec un message d'erreur dans le corps, vous créez exactement le même type de problèmes d'interface que les fonctions C qui pourraient renvoyer un résultat correct ou un code d'erreur, ou des méthodes qui indiquent des problèmes en renvoyant arbitrairementnull. Il est beaucoup plus agréable, en tant qu'utilisateur de votre interface, de signaler les erreurs via un canal "distinct" des retours normaux. Cela s'applique ici aussi, même si les réponses HTTP 200, 404 et 500 sont le même canal du point de vue de bas niveau. Ils sont standardisés et faciles à distinguer afin que mon framework client REST puisse facilement activer ces statuts pour les transformer en structures appropriées dans ma langue; pour faire la même chose avec la couche JSON (où vous dites toujours 200 et donnez-moi un DeviceInfomessage d'erreur ou un message d'erreur), je dois intégrer certaines connaissances des schémas JSON que vous utilisez.

Vous ne devez donc utiliser 200 que lorsque vous pouvez renvoyer une valeur valide du type attendu (raison pour laquelle vous http://example.com/restapi/search-devices?colour=bluepouvez en renvoyer 200 avec un tableau vide s'il n'y a pas de périphériques bleus; un tableau vide est un tableau valide et une réponse pertinente à la demande "I voudrait les détails de tous les appareils bleus "). Si vous ne pouvez pas, utilisez le code de statut non-200 le plus approprié. Même si "le périphérique 123 n'existe pas" est la réponse correcte à "donnez-moi les détails du périphérique 123" et ne constitue pas une erreur pour le serveur , il constitue une exception pour le client dans l'espoir qu'il récupérera DeviceInfoet devrait ne pas être communiquée comme une réponse normale "voici ce que vous avez demandé".


Malheureusement, je suis aussi un client de cette API et je ne peux pas le changer. Ceci est un excellent résumé de la façon dont cela devrait fonctionner, cependant.
JYelton

0

Vous pouvez utiliser l'erreur 422 Entité non traitable pour différencier de 404 non trouvé . 422 signifie que le serveur comprend la demande mais ne peut pas donner une réponse correcte. J'utilise ce code dans des situations similaires.


4
Cet article décrit l'utilisation de 422 plus en détail. Je ne pense pas que ce code d'erreur est vraiment applicable ici.
Robert Harvey

Merci, très utile! Je vais devoir approfondir ce sujet!
FiNeX

0

L'erreur 500 indique normalement que la demande a bloqué un programme côté serveur; dans les environnements d'entreprise, ces erreurs sont traitées comme un œuf au visage et sont évitées.

Une erreur 4xx doit signaler au programmeur que le point de terminaison de l'API (ressource) n'existe pas. Par conséquent, une fois que le point de terminaison approprié est atteint, toute gestion d'erreur à partir de ce moment est la responsabilité du programmeur de l'API, qui doit être gérée correctement, c'est-à-dire avec un message d'erreur d'une réponse de 200.


1
Donc, vous dites "n'utilisez pas 500 parce que vous ne plantez pas le serveur?"
Robert Harvey

0

Bien que le w3 note que 404 est utilisé lorsqu'aucune autre réponse n'est applicable , ne serait-ce pas ce pour quoi une réponse 204 (sans contenu) est bonne? La demande était valide du point de vue du traitement et le serveur a traité la demande et a obtenu un résultat. C'est un succès, qui penche vers des réponses 2xx. Il n'y avait pas de contenu pour cette requête particulière, donc 204 indique à l'utilisateur qu'il n'y a rien d'anormal à leur requête mais qu'il n'y a rien.

Vous pouvez également faire valoir que 409 (Conflict) est une réponse appropriée. Bien que 409 soit le plus souvent utilisé pour un POST, il est dit

La demande n'a pas pu être complétée en raison d'un conflit avec l'état actuel de la ressource. Ce code n'est autorisé que dans les cas où l'utilisateur devrait pouvoir résoudre le conflit et soumettre à nouveau la demande. Le corps de la réponse DEVRAIT inclure suffisamment d'informations pour que l'utilisateur puisse reconnaître la source du conflit. Idéalement, l'entité de réponse inclurait suffisamment d'informations pour permettre à l'utilisateur ou à l'agent d'utilisateur de résoudre le problème. cependant, cela pourrait ne pas être possible et n'est pas requis.

on peut soutenir que la non-existence de l'identifiant demandé est le conflit ici, et le fait de signaler dans le message qu'aucun identifiant de ce type n'existe dans le système est suffisant pour que l'utilisateur puisse reconnaître et résoudre le conflit.


3
Non, la réponse 204 à une demande d'obtention n'aura de sens que lorsque la ressource demandée a été trouvée et que sa représentation comporte exactement zéro caractère.
bdsl

0

Les autres réponses couvrent tout cela, mais je voulais seulement souligner qu'une erreur de la série 500 signifie "je me suis trompé". Dans ce cas, "I" signifie l'API, donc une erreur de serveur non gérée ou similaire. Une erreur de la série 400 signifie "vous vous êtes trompé" - l'appelant de l'API a envoyé quelque chose d'incorrect.


-4

D'autres utilisateurs ont fourni des réponses valides sur ce qu'il faut faire si vous voulez consulter le livre.

Cependant, je suggérerais que vous lisiez trop dans le RESTful et que vous soyez absolument casher à son égard.

REST est un protocole capricieux, car si vous voulez suivre le livre en l'utilisant, il oblige le client et le serveur à se construire en ayant une connaissance spécifique du fait qu'ils communiquent via REST, donc le protocole de communication spécifique étant essentiellement utilisé ne peut pas être extrait.

Il existe une approche différente: évitez complètement RESTfulness et utilisez-le comme un protocole de communication et rien d'autre. Cela signifie que les seules réponses à renvoyer sont "HTTP 200 OK" et "HTTP 500 Internal Server Error" car, en ce qui concerne le protocole de communication, toute tentative de communication ne peut avoir que deux résultats: soit la requête a abouti. livré au serveur, ou non.

Ce qui se passe après la livraison n’est pas du domaine du protocole de communication. Ainsi, une fois que le serveur a reçu la demande et qu'il commence à la traiter, de nombreuses erreurs peuvent survenir, mais il s'agit toutes d'erreurs spécifiques à l'application qui ne relèvent pas du protocole de communication. Ils devraient être communiqués dans le contexte d'une réponse qui semble parfaitement réussie pour autant que le protocole de communication le sache.

En résumé, je recommanderais donc de renvoyer un "HTTP 200 OK" et, dans la charge de réponse ("contenu de la réponse" en langage HTTP), un code d'erreur spécifique à l'application indiquant "ID non trouvé" ou autre.


À en juger par les votes négatifs, je suppose que je dois avoir blessé les sentiments de certaines personnes. Ou peut-être une autre partie de leur corps. C -: =
Mike Nakis

Ils devraient vraiment lire ceci: blogs.dropbox.com/developers/2015/04/…
Mike Nakis

1
Pas mal ici. Vous avez simplement tort. Même l'article que vous avez lié ne mentionne pas l'utilisation de codes d'état de manière tellement fausse que les erreurs et le succès se ressemblent. «En parlant de goût, il est important de se rappeler que la conception d'une API ne concerne pas uniquement les implications pratiques sur les logiciels client et serveur. Le public visé par une API est le développeur qui va la consommer. Selon le« principe du moins Étonnement, "les développeurs auront plus de facilité à apprendre et à comprendre une API si elle respecte les mêmes conventions que les autres API avec lesquelles ils sont familiers."
cHao

@cHao donc, je suppose que la partie qui dit "Dropbox utilise actuellement environ 10 codes d'état différents (8 erreurs et 2 pour réussir), mais nous prévoyons de réduire ce nombre dans une future version de l'API" ne voulait rien dire toi.
Mike Nakis

Je ne voulais pas dire ce que vous en tiriez. S'ils savent ce qu'ils font, alors au plus extrême, ils le réduiraient à un succès et à quatre codes d'erreur (400, 401, 404 et 500), car ils se soucient évidemment des conventions.
cHao
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.