À l’heure actuelle, lorsque vous accédez à copilot.github.com, vous êtes accueilli avec cet exemple :
async function isPositive(text) {
const response = await fetch(`http://text-processing.com/api/sentiment/`, {
method: 'POST',
body: `text=${text}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const json = await response.json();
return json.label === 'pos';
}
C’est mauvais et pourrait entraîner les problèmes de sécurité. Voici où les choses tournent mal : body: `text=${text}`
. Le texte sans échappement est ajouté dans un format avec un encodage défini. C’est similaire à l’injection SQL/HTML, car quelque chose destiné à être une “valeur” peut interagir directement avec le format.
Je vais creuser dans le bon sens, mais aussi me promener dans certaines API moins connues :
URLSearchParams
gère l’encodage et le décodage application/x-www-form-urlencoded
données. C’est plutôt pratique, parce que, eh bien…
Le
application/x-www-form-urlencoded
est à bien des égards une monstruosité aberrante, le résultat de nombreuses années d’accidents de mise en œuvre et de compromis conduisant à un ensemble d’exigences nécessaires à l’interopérabilité, mais ne représentant en aucun cas de bonnes pratiques de conception. En particulier, les lecteurs sont avertis de prêter une attention particulière aux détails tordus impliquant des conversions répétées (et dans certains cas imbriquées) entre les codages de caractères et les séquences d’octets. Malheureusement, le format est largement utilisé en raison de la prévalence des formulaires HTML.— La norme URL
… alors oui, c’est une mauvaise idée d’essayer de l’encoder/décoder vous-même. Voici comment cela fonctionne:
const searchParams = new URLSearchParams();
searchParams.set('foo', 'bar');
searchParams.set('hello', 'world');
console.log(searchParams.toString());
Le constructeur accepte également un tableau de paires nom/valeur, ou un itérateur qui génère des paires nom/valeur :
const searchParams = new URLSearchParams([
['foo', 'bar'],
['hello', 'world'],
]);
console.log(searchParams.toString());
Un objet:
const searchParams = new URLSearchParams({
foo: 'bar',
hello: 'world',
});
console.log(searchParams.toString());
Ou une chaîne :
const searchParams = new URLSearchParams('foo=bar&hello=world');
console.log(searchParams.toString());
Lecture URLSearchParams
Il existe plusieurs méthodes pour lire et muter URLSearchParams
qui sont documentés sur MDN, mais si vous souhaitez gérer toutes les données, son itérateur est pratique :
for (const [key, value] of searchParams) {
console.log(key, value);
}
Ce qui signifie que vous pouvez facilement le convertir en un tableau de paires nom/valeur :
const keyValuePairs = [...searchParams];
Ou utilisez-le avec des API qui prennent en charge les itérateurs qui génèrent des paires nom/valeur, telles que Object.fromEntries
qui le convertit en objet :
const data = Object.fromEntries(searchParams);
Mais sachez que la conversion en objet est parfois une conversion avec perte :
const searchParams = new URLSearchParams([
['foo', 'bar'],
['foo', 'hello'],
]);
console.log(searchParams.toString());
const data = Object.fromEntries(searchParams);
url.searchParams
Les objets URL ont un searchParams
propriété qui est vraiment pratique:
const url = new URL('https://jakearchibald.com/?foo=bar&hello=world');
console.log(url.searchParams.get('hello'));
Malheureusement, location.searchParams
est indéfini. C’est parce que la définition de window.location
est compliqué par la façon dont certaines propriétés de celui-ci fonctionnent à travers les origines. Par exemple paramètre otherWindow.location.href
fonctionne à travers les origines, mais obtenir ce n’est pas permis. Quoi qu’il en soit, pour contourner cela:
location.searchParams;
const url = new URL(location.href);
url.searchParams;
const searchParams = new URLSearchParams(location.search);
URLSearchParams en tant que corps Fetch
Ok, maintenant nous arrivons au point. Le code dans l’exemple au début de l’article est cassé car il n’échappe pas à l’entrée :
const value = 'hello&world';
const badEncoding = `text=${value}`;
console.log([...new URLSearchParams(badEncoding)]);
const correctEncoding = new URLSearchParams({ text: value });
console.log(correctEncoding.toString());
Pour faciliter les choses, URLSearchParams
peut être utilisé directement comme Request
ou Response
body, donc la version “correcte” du code depuis le début de l’article est :
async function isPositive(text) {
const response = await fetch(`http://text-processing.com/api/sentiment/`, {
method: 'POST',
body: new URLSearchParams({ text }),
});
const json = await response.json();
return json.label === 'pos';
}
Si tu utilises URLSearchParams
en tant que corps, le Content-Type
l’en-tête est automatiquement défini sur application/x-www-form-urlencoded
, ce qui est formidable car même après plus de 20 ans d’expérience en tant que développeur Web, je ne me souviens jamais de ce type de contenu. Vous pouvez toujours fournir le vôtre Content-Type
header pour remplacer la valeur par défaut.
Vous ne pouvez pas lire un Request
ou Response
corps comme URLSearchParams
mais il y a des moyens de contourner cela…
FormData
les objets peuvent représenter l’état nom/valeur d’un formulaire HTML. Cela signifie que les valeurs peuvent être des fichiers, comme elles le peuvent avec <input type="file">
.
Vous pouvez remplir FormData
indiquer directement :
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
C’est aussi un itérateur, donc il peut être converti en un tableau de paires nom/valeur, ou un objet, comme vous pouvez le faire avec URLSearchParams
. Mais contrairement à URLSearchParams
vous pouvez lire un formulaire HTML directement comme FormData
:
const formElement = document.querySelector('form');
const formData = new FormData(formElement);
console.log(formData.get('username'));
Cela vous donne les données qui seraient soumises par le formulaire. Je trouve souvent cela beaucoup plus facile que d’obtenir les données de chaque élément individuellement.
FormData en tant que corps Fetch
Semblable à URLSearchParams
vous pouvez utiliser FormData
directement en tant que fetch body :
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
fetch(url, {
method: 'POST',
body: formData,
});
Cela définit automatiquement le Content-Type
en-tête à multipart/form-data
et envoie les données dans ce format :
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
const request = new Request('', { method: 'POST', body: formData });
console.log(await request.text());
… qui enregistre quelque chose comme :
------WebKitFormBoundaryUekOXqmLphEavsu5 Content-Disposition: form-data; name="foo" bar ------WebKitFormBoundaryUekOXqmLphEavsu5 Content-Disposition: form-data; name="hello" world ------WebKitFormBoundaryUekOXqmLphEavsu5--
C’est ce que multipart/form-data
ressemble à. C’est plus complexe que application/x-www-form-urlencoded
, mais il peut inclure des données de fichier. Cependant, certains serveurs ne peuvent pas gérer multipart/form-data
, y compris Express. Si vous voulez soutenir multipart/form-data
dans Express, vous devez utiliser quelque chose comme busboy ou redoutable.
Mais que se passe-t-il si vous voulez envoyer un formulaire en tant que application/x-www-form-urlencoded
? Bien…
Conversion en URLSearchParams
Depuis le URLSearchParams
le constructeur accepte un itérateur qui produit des paires nom/valeur, et FormData
L’itérateur de fait exactement cela, vous pouvez convertir de l’un à l’autre :
const formElement = document.querySelector('form');
const formData = new FormData(formElement);
const searchParams = new URLSearchParams(formData);
fetch(url, {
method: 'POST',
body: searchParams,
});
Cependant, cette conversion sera lancée si les données du formulaire contiennent un fichier. application/x-www-form-urlencoded
ne peut pas représenter les données de fichier, donc ni l’un ni l’autre ne peut URLSearchParams
.
Lecture des corps Fetch en tant que FormData
Vous pouvez également lire un Request
ou Response
objet comme FormData
:
const formData = await request.formData();
Cela fonctionne si le corps de la requête/réponse est multipart/form-data
ou application/x-www-form-urlencoded
. C’est particulièrement utile pour gérer les soumissions de formulaires dans un service worker.
Il existe quelques autres formats qui peuvent être récupérés :
Blobs
Blob
objets (et donc File
puisqu’il hérite de Blob
) peut être récupérer des corps :
fetch(url, {
method: 'POST',
body: blob,
});
Cela définit automatiquement le Content-Type
à la valeur de blob.type
.
Cordes
fetch(url, {
method: 'POST',
body: JSON.stringify({ hello: 'world' }),
headers: { 'Content-Type': 'application/json' },
});
Cela définit automatiquement le Content-Type
pour text/plain;charset=UTF-8
mais comme toujours, il peut être remplacé, comme je l’ai fait ci-dessus.
Tampons
ArrayBuffer
objets, et tout ce qui est soutenu par un tableau tampon tel que Uint8Array
peut être utilisé comme corps d’extraction :
fetch(url, {
method: 'POST',
body: new Uint8Array([
]),
headers: { 'Content-Type': 'image/png' },
});
Cela ne définit pas le Content-Type
header automatiquement, vous devez donc le faire vous-même.
Ruisseaux
Et enfin, les corps récupérés peuvent être des flux ! Pour Response
objets, cela permet toutes sortes de divertissements avec un travailleur de service, et plus récemment, ils peuvent également être utilisés avec des requêtes.
Alors oui, n’essayez pas de gérer multipart/form-data
ou application/x-www-form-urlencoded
vous-même, laissez FormData
et URLSearchParams
faites le dur labeur!
Je ne suis pas non plus contre des choses comme GitHub Copilot. Traitez simplement la sortie comme une réponse sur StackOverflow et examinez-la avant de la valider.
Nicholas Mendez m’a tweeté demander comment FormData
peut être sérialisé en JSON sans perte de données.
Les formulaires peuvent contenir des champs comme celui-ci :
<select multiple name="tvShows">
<option>Motherland</option>
<option>Taskmaster</option>
…
</select>
…où plusieurs valeurs peuvent être sélectionnées, ou vous pouvez avoir plusieurs entrées avec le même nom :
<fieldset>
<legend>TV Shows</legend>
<label>
<input type="checkbox" name="tvShows" value="Motherland" />
Motherland
</label>
<label>
<input type="checkbox" name="tvShows" value="Taskmaster" />
Taskmaster
</label>
…
</fieldset>
Le résultat est un FormData
objet qui a plusieurs entrées avec le même nom, comme ceci :
const formData = new FormData();
formData.append('foo', 'bar');
formData.append('tvShows', 'Motherland');
formData.append('tvShows', 'Taskmaster');
Et comme nous l’avons vu avec URLSearchParams
certaines conversions d’objets entraînent des pertes :
const data = Object.fromEntries(formData);
Il existe plusieurs façons d’éviter la perte de données et de se retrouver avec quelque chose de JSON-stringifyable. Tout d’abord, il y a le tableau des paires nom/valeur :
const data = [...formData];
Mais si vous voulez un objet plutôt qu’un tableau, vous pouvez faire ceci :
const data = Object.fromEntries(
[...new Set(formData.keys())]
.map((key) => [key, formData.getAll(key)]),
);
… ce qui vous donne :
{
"foo": ["bar"],
"tvShows": ["Motherland", "Taskmaster"]
}
J’aime que chaque valeur soit un tableau, même s’il n’a qu’un seul élément. Cela empêche beaucoup de ramifications de code sur le serveur et simplifie la validation. Cependant, vous préférerez peut-être la convention PHP/Perl où un nom de champ qui se termine par []
signifie “cela devrait produire un tableau”:
<select multiple name="tvShows[]">
…
</select>
Et pour le convertir :
const data = Object.fromEntries(
[...new Set(formData.keys())].map((key) =>
key.endsWith('[]')
?
[key.slice(0, -2), formData.getAll(key)]
:
[key, formData.get(key)],
),
);
… ce qui vous donne :
{
"foo": "bar",
"tvShows": ["Motherland", "Taskmaster"]
}
Rappelez-vous, n’essayez pas de convertir un formulaire en JSON si le formulaire contient des fichiers. Si tel est le cas, vous êtes bien mieux avec multipart/form-data
.