Query string ficelle

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Cher lecteur, veuillez pardonner l’intitulé tapageur de ce billet dont le seul objectif est de générer un maximum de trafic sur ce site à la fréquentation en berne.

tl;dr

Si plusieurs clés portant le même nom existent dans la query string dans le but de récupérer une liste, certains langages et frameworks tels que Php et Ruby on Rails s’attendent à une syntaxe à base de crochets :

?tag[]=1&tag[]=2

Alors que Django attend la syntaxe :

?tag=1&tag=2

Or, depuis jQuery 1.4 (ok c’est OLD), jQuery.param() sérialise par défaut sur la base de la syntaxe à crochets.

Par conséquent, pour assurer une compatibilité maximale entre un back-end Django et l’API jQuery.ajax il vaut mieux toujours utiliser :

jQuery.ajaxSettings.traditional = true;

Introduction

Lors de la mise en place et de la consommation d’une API Rest sur la base de Django REST framework et de Django Filter (via l’utilisation de ModelMultipleChoiceFilter) je me suis rendu compte d’un point important relatif au traitement par Django des query string contenant plusieurs fois la même clé :

?tag=1&tag=2

Quand peut-on avoir plusieurs fois la même clé dans la query string ?

Essayez de soumettre ce formulaire par exemple :

<form method="get">
    <input type="checkbox" name="a" value="1">
    <input type="checkbox" name="a" value="2">
    <input type="checkbox" name="a" value="3">
    <input type="submit" value="Submit">
</form>

Ou bien celui-ci :

<form method="get">
    <select multiple name="a">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
        <option value="4">4</option>
    </select>
    <input type="submit" value="Submit">
</form>

Position officielle

La RFC 3986 concernant la syntaxe des URI ne précise rien :

There is no spec on this.

Du coup chaque outil fait sa propre sauce et certains utilisent une syntaxe à base de crochets.

Pourquoi cette syntaxe à base de crochets ?

À cause de PHP je pense. Je ne trouve pas d’information sur l’origine de cette syntaxe.

La documentation de parse_str est laconique. Et comme dirait l’autre dans PHP: a fractal of bad design :

parse_str parses a query string, with no indication of this in the name

Laissons maintenant la parole à Evan K qui parle de bizarre php-specific behavior :

It bears mentioning that the parse_str builtin does NOT process a query string in the CGI standard way, when it comes to duplicate fields. If multiple fields of the same name exist in a query string, every other web processing language would read them into an array, but PHP silently overwrites them

Rails (via Rack) semble s’être basé sur le même comportement que PHP. Et Rack paie sa doc ! Plus concis tu meurs :D

Certains qualifient cette syntaxe de Magical Parameters :

Magical parameters by default is a bad thing.

In the general case, it’s unexpected behavior. A curve ball. Many developers may not even know this stuff happens. How should they know? It’s undocumented!

Et ça peut finir en truc de ouf !

jQuery trouve que c’est “moderne”

Dans jQuery 1.4 $.param demystified l’auteur nous explique pourquoi jQuery a décidé d’adopter la syntaxe à base de crochets :

So, I spend a lot of time in the #jquery irc.freenode.net support channel helping newbies, and a very common newbie mistake is how people try to submit forms with array values in PHP or Rails

Enfoirés de newbies ! :D

Donc si vous n’utilisez pas PHP ou Rails, vous utilisez un framework traditionnel, lol !

Démonstration avec un framework “moderne”, documentation jQuery dixit :

> $.param({ a: [1, 2] })
"a[]=1&a[]=2"

Puis avec un framework “traditionnel”, jQuery.param(obj, traditional) :

> $.param({ a: [1, 2] }, true)
"a=1&a=2"

Alors vous espèce de sale conservateur avec votre framework conventionnel old school, utilisez donc le paramètre traditional des settings de l’API jQuery.ajax.

jQuery.ajaxSettings.traditional = true;

Personnellement je trouve le paramètre traditional mal nommé. withSquareBrackets aurait été plus explicite.

Bref.

Si vous utilisez Backbone.js pour parler à un back-end Django avec Backbone.sync configuré par défaut, alors vous utilisez l’API jQuery.ajax. Configurez Backbone.sync avec $.ajaxSettings.traditional pour une compatibilité maximale avec Django.

Django, QueryDict et MultiValueDict

La query string passée à Django par le navigateur est parsée et stockée dans un objet QueryDict. QueryDict hérite de MultiValueDict qui est une structure de données intéressante permettant d’avoir plusieurs valeurs pour la même clé dans un dictionnaire :

Django uses MultiValueDict to handle this case, basing its default behavior on what most other frameworks do in this situation. By default, accessing a key in a MultiValueDict returns the last value that was submitted with that name. If all the values are required, a separate getlist() method is available to return the full list, even if it only contains one item.

À la différence de PHP qui a besoin de modifier le nom de la clé pour savoir s’il faut récupérer une liste, le QueryDict de Django permet d’utiliser getlist() :

In [1]: from django.http import HttpRequest, QueryDict
In [2]: request = HttpRequest()
In [3]: request.GET = QueryDict('tag=1&tag=2')
In [4]: request.GET['tag']
Out[4]: u'2'
In [5]: request.GET.getlist('tag')
Out[5]: [u'1', u'2']

Voyez les widgets MultipleHiddenInput et SelectMultiple, puis suivez les commentaires dans le code :

This class exists to solve the irritating problem raised by cgi.parse_qs, which returns a list for every key, even though most Web forms submit single name-value pairs.

Donc direction cgi.parseqs urlparse.parseqs pour voir comment ça se passe un peu plus bas dans la pile.

HTTP Parameter Pollution

Pour finir il faut noter que la possibilité de passer plusieurs fois la même clé dans la query string peut être à l’origine d’une attaque nommée HTTP Parameter pollution [PDF] et présentée à l’OWASP AppSec Europe 2009. Les mecs de la sécu sont tellement en avance qu’ils publient toujours leurs papiers au format PDF :D

Cette attaque est basée sur la différence de traitement des paramètres multiples dans la query string par les différents back-ends HTTP.

Du coup si vous attendez des données en GET, en PUT ou en POST, vous pouvez évaluer les risques.

Ressources complémentaires

Avant Backbone.js, retour d’expérience Après TextMate 2

Tag Kemar Joint