Client HTTP
Ajout de champs au schéma GraphQL pour exécuter des requêtes HTTP contre un serveur web et récupérer leur réponse :
_sendJSONObjectItemHTTPRequest_sendJSONObjectItemHTTPRequests_sendJSONObjectCollectionHTTPRequest_sendJSONObjectCollectionHTTPRequests_sendHTTPRequest_sendHTTPRequests_sendGraphQLHTTPRequest_sendGraphQLHTTPRequests
Pour des raisons de sécurité, les URLs auxquelles il est possible de se connecter doivent être explicitement configurées.
Liste des champs
Les champs suivants sont ajoutés au schéma.
_sendJSONObjectItemHTTPRequest
Récupère la réponse (REST) pour un seul objet JSON.
Signature : _sendJSONObjectItemHTTPRequest(input: HTTPRequestInput!): JSONObject.
_sendJSONObjectItemHTTPRequests
Récupère la réponse (REST) pour un seul objet JSON depuis plusieurs endpoints, exécutés de manière asynchrone (en parallèle) ou synchrone (l'un après l'autre).
Signature : _sendJSONObjectItemHTTPRequests(async: Boolean = true, inputs: [HTTPRequestInput!]!): [JSONObject].
_sendJSONObjectCollectionHTTPRequest
Récupère la réponse (REST) pour une collection d'objets JSON.
Signature : _sendJSONObjectCollectionHTTPRequest(input: HTTPRequestInput!): [JSONObject].
_sendJSONObjectCollectionHTTPRequests
Récupère la réponse (REST) pour une collection d'objets JSON depuis plusieurs endpoints, exécutés de manière asynchrone (en parallèle) ou synchrone (l'un après l'autre).
Signature : _sendJSONObjectCollectionHTTPRequests(async: Boolean = true, inputs: [HTTPRequestInput!]!): [[JSONObject]].
_sendHTTPRequest
Se connecte à l'URL spécifiée et récupère un objet HTTPResponse, qui contient les champs suivants :
statusCode: Int!contentType: String!body: String!headers: JSONObject!header(name: String!): StringhasHeader(name: String!): Boolean!
Signature : _sendHTTPRequest(input: HTTPRequestInput!): HTTPResponse.
_sendHTTPRequests
Similaire à _sendHTTPRequest mais reçoit plusieurs URLs, et permet de s'y connecter de manière asynchrone (en parallèle).
Signature : _sendHTTPRequests(async: Boolean = true, inputs: [HTTPRequestInput!]!): [HTTPResponse].
_sendGraphQLHTTPRequest
Exécute une requête GraphQL contre l'endpoint fourni, et récupère la réponse sous forme d'objet JSON.
L'input de ce champ accepte les données attendues pour GraphQL : l'endpoint, la requête GraphQL, les variables et le nom de l'opération, et définit déjà la méthode par défaut (POST) et le content type (application/json).
Signature : _sendGraphQLHTTPRequest(input: GraphQLRequestInput!): JSONObject.
_sendGraphQLHTTPRequests
Similaire à _sendGraphQLHTTPRequests mais exécute plusieurs requêtes GraphQL de manière concurrente, que ce soit de manière asynchrone (en parallèle) ou synchrone (l'une après l'autre).
Signature : _sendGraphQLHTTPRequests(async: Boolean = true, inputs: [GraphQLRequestInput!]!): JSONObject.
Configuration des URLs autorisées
Nous devons configurer la liste des URLs auxquelles nous pouvons nous connecter.
Chaque entrée peut être :
- Une regex (expression régulière), si elle est entourée par
/ou#, ou - L'URL complète, sinon
Par exemple, l'une de ces entrées correspond à l'URL "https://gatographql.com/recipes/" :
https://gatographql.com/recipes/#https://gatographql.com/recipes/?##https://gatographql.com/.*#/https:\\/\\/gatographql.com\\/(\S+)/
Il y a 2 endroits où cette configuration peut avoir lieu, par ordre de priorité :
- Personnalisée : Dans la Configuration du schéma correspondante
- Générale : Dans la page des Réglages
Dans la Configuration du schéma appliquée à l'endpoint, sélectionnez l'option "Utiliser une configuration personnalisée" et saisissez ensuite les entrées souhaitées :

Sinon, les entrées définies dans l'onglet "Send HTTP Request Fields" des Réglages seront utilisées :

Il y a 2 comportements, "Autoriser l'accès" et "Refuser l'accès" :
- Autoriser l'accès : seules les entrées configurées sont accessibles, aucune autre ne l'est
- Refuser l'accès : les entrées configurées ne sont pas accessibles, toutes les autres le sont

Capacité requise pour accéder aux URLs internes
Certaines URLs se résolvent vers des adresses internes (127.0.0.1, plages link-local, endpoints de cloud-metadata, etc.) qui peuvent exposer des services internes si elles sont atteintes. Ce réglage est configuré dans la page des Réglages, sous Plugin Configuration > HTTP Client.

Capacité WordPress que l'utilisateur demandeur doit avoir pour cibler des URLs qui se résolvent vers des adresses internes (127.0.0.1, plages link-local, endpoints de cloud-metadata, etc.).
Par défaut manage_options, afin que les utilisateurs non-administrateurs ne puissent pas atteindre les services internes via les champs du Client HTTP.
Sélectionnez (tout utilisateur connecté) pour désactiver la vérification de capacité.
Quand utiliser chaque champ
Tous les champs sont similaires mais différents.
_sendJSONObjectItemHTTPRequest
Ce champ récupère un élément d'objet JSON, ce qui est utile lors de l'interrogation d'un seul élément depuis un endpoint REST, comme depuis l'endpoint de la WP REST API /wp-json/wp/v2/posts/1/.
Cette requête :
{
postData: _sendJSONObjectItemHTTPRequest(input: { url: "https://newapi.getpop.org/wp-json/wp/v2/posts/1/" } )
}...récupère cette réponse :
{
"data": {
"postData": {
"id": 1,
"date": "2019-08-02T07:53:57",
"date_gmt": "2019-08-02T07:53:57",
"guid": {
"rendered": "https:\/\/newapi.getpop.org\/?p=1"
},
"modified": "2021-01-14T13:18:39",
"modified_gmt": "2021-01-14T13:18:39",
"slug": "hello-world",
"status": "publish",
"type": "post",
"link": "https:\/\/newapi.getpop.org\/uncategorized\/hello-world\/",
"title": {
"rendered": "Hello world!"
},
"content": {
"rendered": "\n<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!<\/p>\n\n\n\n<p>I’m demonstrating a Youtube video:<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Introduction to the Component-based API by Leonardo Losoviz | JSConf.Asia 2019\" width=\"750\" height=\"422\" src=\"https:\/\/www.youtube.com\/embed\/9pT-q0SSYow?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n<\/div><figcaption>This is my presentation in JSConf Asia 2019<\/figcaption><\/figure>\n",
"protected": false
},
"excerpt": {
"rendered": "<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing! I’m demonstrating a Youtube video:<\/p>\n",
"protected": false
},
"author": 1,
"featured_media": 0,
"comment_status": "closed",
"ping_status": "open",
"sticky": false,
"template": "",
"format": "standard",
"meta": [],
"categories": [
1
],
"tags": [
193,
173
]
}
}
}_sendJSONObjectCollectionHTTPRequest
Ce champ est similaire à _sendJSONObjectItemHTTPRequest, mais récupère une collection d'objets JSON, comme depuis l'endpoint de la WP REST API /wp-json/wp/v2/posts/.
Cette requête :
{
postData: _sendJSONObjectItemHTTPRequest(input: { url: "https://newapi.getpop.org/wp-json/wp/v2/posts/?per_page=3&_fields=id,type,title,date" } )
}...récupère cette réponse :
{
"data": {
"postData": [
{
"id": 1692,
"date": "2022-04-26T10:10:08",
"type": "post",
"title": {
"rendered": "My Blogroll"
}
},
{
"id": 1657,
"date": "2020-12-21T08:24:18",
"type": "post",
"title": {
"rendered": "A tale of two cities – teaser"
}
},
{
"id": 1499,
"date": "2019-08-08T02:49:36",
"type": "post",
"title": {
"rendered": "COPE with WordPress: Post demo containing plenty of blocks"
}
}
]
}
}_sendHTTPRequest
Ce champ récupère un objet HTTPResponse avec toutes les propriétés de la réponse, afin que nous puissions interroger indépendamment le body (qui est de type String, c'est-à-dire qu'il n'est pas casté en JSON), le code de statut, le content type et les headers.
Par exemple, la requête suivante :
{
_sendHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/wp/v2/comments/11/?_fields=id,date,content"
}
) {
statusCode
contentType
headers
body
contentLengthHeader: header(name: "Content-Length")
cacheControlHeader: header(name: "Cache-Control")
}
}...renvoie cette réponse :
{
"data": {
"_sendHTTPRequest": {
"statusCode": 200,
"contentType": "application\/json; charset=UTF-8",
"headers": {
"Access-Control-Allow-Headers": "Authorization, X-WP-Nonce, Content-Disposition, Content-MD5, Content-Type",
"Access-Control-Expose-Headers": "X-WP-Total, X-WP-TotalPages, Link",
"Allow": "GET",
"Cache-Control": "max-age=300,no-store",
"Content-Length": "508"
},
"body": "{\"id\":11,\"date\":\"2020-12-12T04:09:36\",\"content\":{\"rendered\":\"<p>Wow, this sounds awesome!<\\\/p>\\n\"},\"_links\":{\"self\":[{\"href\":\"https:\\\/\\\/newapi.getpop.org\\\/wp-json\\\/wp\\\/v2\\\/comments\\\/11\"}],\"collection\":[{\"href\":\"https:\\\/\\\/newapi.getpop.org\\\/wp-json\\\/wp\\\/v2\\\/comments\"}],\"author\":[{\"embeddable\":true,\"href\":\"https:\\\/\\\/newapi.getpop.org\\\/wp-json\\\/wp\\\/v2\\\/users\\\/3\"}],\"up\":[{\"embeddable\":true,\"post_type\":\"post\",\"href\":\"https:\\\/\\\/newapi.getpop.org\\\/wp-json\\\/wp\\\/v2\\\/posts\\\/28\"}]}}",
"contentLengthHeader": "508",
"cacheControlHeader": "max-age=300,no-store"
}
}
}_sendGraphQLHTTPRequest
Exécution de la requête suivante :
{
graphQLRequest: _sendGraphQLHTTPRequest(
input: {
endpoint: "https://newapi.getpop.org/api/graphql/"
query: """
query GetPosts($postIDs: [ID]!) {
posts(filter: { ids: $postIDs }) {
id
title
}
}
"""
variables: [
{
name: "postIDs",
value: [1, 1499]
}
]
}
)
}...renvoie la réponse suivante :
{
"data": {
"graphQLRequest": {
"data": {
"posts": [
{
"id": 1499,
"title": "COPE with WordPress: Post demo containing plenty of blocks"
},
{
"id": 1,
"title": "Hello world!"
}
]
}
}
}
}Champs à requêtes multiples : _sendJSONObjectItemHTTPRequests, _sendJSONObjectCollectionHTTPRequests, _sendGraphQLHTTPRequests et _sendHTTPRequests
Ces champs fonctionnent de manière similaire à leurs champs non-multiples correspondants, mais récupèrent des données depuis plusieurs endpoints à la fois, soit de manière asynchrone (en parallèle) soit de manière synchrone (l'un après l'autre). Les réponses sont placées dans une liste, dans le même ordre que celui dans lequel les URLs ont été définies dans le paramètre urls.
Par exemple, la requête suivante :
{
weatherForecasts: _sendJSONObjectItemHTTPRequests(
urls: [
"https://api.weather.gov/gridpoints/TOP/31,80/forecast",
"https://api.weather.gov/gridpoints/TOP/41,55/forecast"
]
)
}...produit cette réponse :
{
"data": {
"weatherForecasts": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-97.1089731,
39.766826299999998
],
[
-97.108526900000001,
39.744778799999999
]
]
]
},
"properties": {
"updated": "2022-03-04T09:39:46+00:00",
"units": "us",
"forecastGenerator": "BaselineForecastGenerator",
"generatedAt": "2022-03-04T10:31:47+00:00",
"updateTime": "2022-03-04T09:39:46+00:00",
"validTimes": "2022-03-04T03:00:00+00:00/P7DT22H",
"elevation": {
"unitCode": "wmoUnit:m",
"value": 441.95999999999998
}
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-96.812529900000001,
39.218048000000003
],
[
-96.812148500000006,
39.195940300000004
]
]
]
},
"properties": {
"updated": "2022-03-04T09:39:46+00:00",
"units": "us",
"forecastGenerator": "BaselineForecastGenerator",
"generatedAt": "2022-03-04T10:42:26+00:00",
"updateTime": "2022-03-04T09:39:46+00:00",
"validTimes": "2022-03-04T03:00:00+00:00/P7DT22H",
"elevation": {
"unitCode": "wmoUnit:m",
"value": 409.04160000000002
}
}
}
]
}
}Exécution synchrone vs asynchrone
Ces champs nous permettent d'exécuter plusieurs requêtes :
_sendHTTPRequests_sendJSONObjectItemHTTPRequests_sendJSONObjectCollectionHTTPRequests_sendGraphQLHTTPRequests
Ces champs reçoivent l'input $async, pour définir si les requêtes doivent être exécutées de manière synchrone ($async => false) ou asynchrone.
Exécution synchrone
Les requêtes HTTP sont exécutées dans l'ordre, chacune s'exécutant juste après que la précédente a été résolue.
Lorsque toutes les requêtes HTTP réussissent, le champ imprimera un tableau avec leurs réponses, dans le même ordre qu'elles apparaissent dans la liste d'entrée.
Si une requête HTTP échoue, l'exécution s'arrête immédiatement, c'est-à-dire que les requêtes HTTP suivantes dans la liste d'entrée ne sont pas exécutées.
Quelques causes possibles d'échec des requêtes HTTP :
- Le serveur auquel se connecter est hors ligne
- Le code de statut de la réponse n'est pas 200 : une erreur interne 500, un 404 non trouvé, un 403 interdit, etc.
- Le content type de la réponse n'est pas
application/json
(Ces deux derniers sont traités comme une erreur par _sendJSONObjectItemHTTPRequests, _sendJSONObjectCollectionHTTPRequests et _sendGraphQLHTTPRequests, qui s'attendent à ne gérer que des types JSON, mais pas par _sendHTTPRequests, qui n'impose pas de contrainte.)
En cas d'erreur, le champ renvoie null (c'est-à-dire que la réponse de toute requête HTTP réussie précédente ne sera pas imprimée), et l'entrée d'erreur contiendra l'extension httpRequestInputArrayPosition pour indiquer quel est l'élément de la liste d'entrée qui a échoué (en commençant à 0) :
{
"errors": [
{
"message": "Server error: `GET https:\/\/mysite.com\/page-triggering-some-500-error` resulted in a `500 Internal Server Error` response",
"extensions": {
"httpRequestInputArrayPosition": 0,
"field": "_sendJSONObjectItemHTTPRequests(async: false, inputs: [{url: \"https:\/\/mysite.com\/page-triggering-some-500-error\"}, {url: \"https:\/\/mysite.com\/wp-json\/wp\/v2\/posts\/1\/\"}, {url: \"https:\/\/mysite.com\/wp-json\/wp\/v2\/users\/1\/\"}])"
}
}
],
"data": {
"_sendJSONObjectItemHTTPRequests": null
}
}Exécution asynchrone
Toutes les requêtes HTTP sont exécutées de manière concurrente (c'est-à-dire en parallèle), et l'ordre dans lequel les requêtes HTTP seront résolues n'est pas connu à l'avance.
Lorsque toutes les requêtes HTTP réussissent, le champ imprimera un tableau avec leurs réponses, dans le même ordre qu'elles apparaissent dans la liste d'entrée.
Dès qu'une requête HTTP échoue, l'exécution s'arrête immédiatement, cependant à ce moment toutes les autres requêtes HTTP peuvent déjà avoir été exécutées également.
De plus, le serveur n'indiquera pas quel est l'élément de la liste qui a échoué (remarquez qu'il n'y a pas d'extension httpRequestInputArrayPosition dans la réponse ci-dessous) :
{
"errors": [
{
"message": "Server error: `GET https:\/\/mysite.com\/page-triggering-some-500-error` resulted in a `500 Internal Server Error` response",
"extensions": {
"field": "_sendJSONObjectItemHTTPRequests(async: true, inputs: [{url: \"https:\/\/mysite.com\/page-triggering-some-500-error\"}, {url: \"https:\/\/mysite.com\/wp-json\/wp\/v2\/posts\/1\/\"}, {url: \"https:\/\/mysite.com\/wp-json\/wp\/v2\/users\/1\/\"}])"
}
}
],
"data": {
"_sendJSONObjectItemHTTPRequests": null
}
}Champs globaux
Tous ces champs sont des Global Fields, de sorte qu'ils sont ajoutés à chaque type dans le schéma GraphQL : dans QueryRoot, mais aussi dans Post, User, Comment, etc.
Cela nous permet de nous connecter à un endpoint d'API externe généré à l'exécution dans la même requête GraphQL, en fonction des données stockées sur une entité.
Par exemple, nous pouvons itérer une liste d'utilisateurs dans notre base de données et, pour chacun, se connecter à un système externe (comme un CRM) pour récupérer des données supplémentaires sur eux.
Dans cette requête, nous générons l'endpoint de l'API en utilisant la fonctionnalité Field to Input et le champ function _arrayJoin :
{
users(
pagination: { limit: 2 },
sort: { order: ASC, by: ID }
) {
id
endpoint: _arrayJoin(values: [
"https://newapi.getpop.org/wp-json/wp/v2/users/",
$__id,
"?_fields=name"
])
_sendJSONObjectItemHTTPRequest(input: { url: $__endpoint } )
}
}...produisant :
{
"data": {
"users": [
{
"id": 1,
"endpoint": "https://newapi.getpop.org/wp-json/wp/v2/users/1?_fields=name",
"_sendJSONObjectItemHTTPRequest": {
"name": "leo",
"_links": {
"self": [
{
"href": "https://newapi.getpop.org/wp-json/wp/v2/users/1"
}
],
"collection": [
{
"href": "https://newapi.getpop.org/wp-json/wp/v2/users"
}
]
}
}
},
{
"id": 2,
"endpoint": "https://newapi.getpop.org/wp-json/wp/v2/users/2?_fields=name",
"_sendJSONObjectItemHTTPRequest": {
"name": "themedemos",
"_links": {
"self": [
{
"href": "https://newapi.getpop.org/wp-json/wp/v2/users/2"
}
],
"collection": [
{
"href": "https://newapi.getpop.org/wp-json/wp/v2/users"
}
]
}
}
}
]
}
}