HTTP Client
HTTP ClientClient HTTP

Client HTTP

Included in the “Power Extensions” bundle

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!): String
  • hasHeader(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é :

  1. Personnalisée : Dans la Configuration du schéma correspondante
  2. 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 :

Définition des entrées pour la Configuration du schéma

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

Définition des entrées dans les Réglages
Définition des entrées dans les Réglages

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
Définition du comportement d'accès
Définition du comportement d'accès

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.

Définition de la capacité requise pour accéder aux URLs internes
Définition de la capacité requise pour accéder aux URLs internes

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&#8217;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&#8217;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 &#8211; 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"
              }
            ]
          }
        }
      }
    ]
  }
}