restclient — n_pbt_restclient #
← Référence des composants · Sommaire du guide
Un client HTTPS pour PowerBuilder, la version 10 comprise : GET, POST, PUT, PATCH, DELETE, les en-têtes, un corps JSON, le téléchargement d'un fichier. Natif — le TLS, le proxy et la décompression du système, et aucun mur CORS, parce que la requête ne part pas d'une page.
▶ Le voir en vrai — Application de démonstration, tuile REST client : les réponses, le code qui les obtient et cette page, côte à côte (connexion Internet requise).
En bref #
| Objet non visuel | n_pbt_restclient |
| Sert à | Appeler une API REST depuis une application PowerBuilder : lire, créer, mettre à jour, télécharger |
| Principe | Une requête est un appel qui rend le statut HTTP ; la réponse attend dans l'objet, lue par of_response_text ou of_json_value |
| Dépendance | WinHTTP, dans la DLL de la bibliothèque — aucune page, aucun runtime supplémentaire |
Démarrage rapide #
// Local variables
n_pbt_restclient lnv_rest
n_pbt_json lnv_j
string ls_token, ls_customer
long ll_status
// One client for the API : its address and the token it sends (here read from the settings)
lnv_rest = create n_pbt_restclient
lnv_rest.is_base_url = "https://api.example.com"
ls_token = ProfileString("app.ini", "api", "token", "")
lnv_rest.of_set_bearer(/*token*/ ls_token)
// Read : the status comes back, the body waits in the object
ll_status = lnv_rest.of_get(/*url*/ "/orders/4152")
if ll_status = 200 then ls_customer = lnv_rest.of_json_value(/*path*/ "customer")
// Create : the body is built with n_pbt_json, never by hand
lnv_j.of_set_number(/*path*/ "order", /*value*/ 4152)
lnv_j.of_set_string(/*path*/ "status", /*value*/ "shipped")
ll_status = lnv_rest.of_post(/*url*/ "/shipments", /*body*/ lnv_j.of_text())
if ll_status < 0 then MessageBox("API", lnv_rest.of_get_last_error())
// The client is no longer needed
destroy lnv_rest
Chaque méthode HTTP rend le statut que le serveur a choisi — un 404 est une réponse, pas une erreur — ou un code négatif quand la requête n'a pas abouti : -5 URL ou en-tête invalide, certificat client introuvable, -4 échec (pas de réseau, nom inconnu, délai dépassé, TLS, corps de texte de plus de 64 Mo — 16 Mo dans un processus 32 bits ; of_get_last_error() dit lequel), -6 refusé en mode démo. -2 dit que le client n'a pas pu être créé. Un objet créé par create suffit pour le synchrone ; pour les événements de l'asynchrone, voyez « Où écrire les événements », plus bas.
Pourquoi natif, et pas la page #
Un fetch() lancé depuis une page de la bibliothèque est soumis à CORS : toute API qui n'envoie pas Access-Control-Allow-Origin lui est fermée, et les API d'entreprise ne l'envoient pas. La requête part donc de la DLL, par WinHTTP : le TLS du système et ses certificats, le proxy configuré, gzip. Ce que vous perdez : rien ; ce que vous gagnez : toutes les API.
Propriétés #
| Propriété | Type | Défaut | Rôle |
|---|---|---|---|
is_base_url | string | "" | Mis devant une URL relative : is_base_url = "https://api.example.com" puis of_get("/orders/4152"). Une URL absolue est prise telle quelle |
il_timeout_ms | long | 30000 | Délai de chaque étape d'une requête : résolution du nom, connexion, envoi, puis attente de chaque bloc de la réponse. Un gros téléchargement qui avance n'est jamais coupé ; un serveur muet pendant ce délai l'est : -4 et « timed out ». 0 ou moins vaut le défaut, 30 s : il n'existe pas de « pas de délai » |
il_status | long | 0 | Le statut HTTP de la dernière requête synchrone, ou son code négatif ; la valeur que la requête a rendue. Une réponse asynchrone a le sien : of_status(id) |
ib_keep_cookies | boolean | true | Garde les cookies d'une requête à l'autre — synchrones et asynchrones, qui partagent une même session : une API qui pose un cookie à la connexion vous garde connecté. À false, plus aucun cookie n'est envoyé ni gardé, et ceux déjà gardés sont oubliés ; appliqué tout de suite |
il_max_retries | long | 0 | Combien de fois une requête asynchrone est rejouée sur 429, 503 ou une panne réseau. Un POST dont la réponse s'est perdue n'est jamais rejoué : il a pu être traité. Un échec qui se reproduirait à l'identique n'est jamais rejoué : trop de redirections, redirection invalide, redirection HTTPS → HTTP refusée, échec TLS, fichier impossible à écrire |
il_retry_backoff_ms | long | 500 | L'attente avant la première tentative, en millisecondes, doublée à chaque essai et plafonnée à 60 s. Un en-tête Retry-After du serveur (secondes ou date) passe devant |
ib_windows_auth | boolean | false | Authentification Windows intégrée (Negotiate / NTLM) : quand un serveur la demande — une API IIS de l'intranet —, les identifiants de la session Windows répondent, sans rien saisir. Désactivée par défaut : sans elle, ils ne partent jamais, vers aucun serveur. Lue à chaque requête ; seuls l'hôte et le port de la requête y répondent : une redirection vers un autre hôte repart sans eux |
is_client_certificate | string | "" | Un certificat client (TLS mutuel) : l'empreinte SHA-1 d'un certificat du magasin personnel — celui de l'utilisateur, puis celui de la machine —, telle que le gestionnaire de certificats l'affiche ; espaces et deux-points ignorés. Vide = aucun. Une empreinte introuvable fait rendre -5 à toute requête (« client certificate not found »). Lue à chaque requête. Il n'existe pas d'option pour ignorer une erreur de certificat du serveur ; il n'est présenté qu'à l'hôte et au port de la requête, jamais après une redirection ailleurs |
ib_check_revocation | boolean | false | Refuse un certificat de serveur que son autorité a révoqué : la liste de révocation ou l'OCSP de la chaîne est consulté. Désactivé par défaut : quand la vérification ne peut pas se faire — l'autorité injoignable depuis un réseau fermé —, la requête échoue (-4). Lu à chaque requête |
is_last_request | string | "" | Ce que la dernière requête a demandé, à lire quand une API répond 400 : la méthode et l'URL, puis les en-têtes posés par votre code — leurs secrets masqués : le schéma d'un Authorization reste (Bearer ***), la valeur d'un en-tête dont le nom dit key, token, secret, password ou cookie disparaît —, puis la taille du corps. La bibliothèque ajoute User-Agent, Content-Type et les cookies |
ipo_owner | powerobject | null | L'objet visuel pour lequel ce client travaille : la licence se vérifie sur sa classe. Nécessaire seulement dans l'application de démonstration ; une clé de développement ou d'exécution débride le client sans lui. Non débridé, le client est en mode démo — corps coupés à 4096 caractères, ni téléchargement ni téléversement |
Constantes : METHOD_GET, METHOD_POST, METHOD_PUT, METHOD_PATCH, METHOD_DELETE pour of_request.
Méthodes #
| Méthode | Rôle |
|---|---|
of_get_last_error ( ) → string | Pourquoi la dernière requête synchrone — ou un appel asynchrone refusé sur-le-champ — a rendu un code négatif. Vide après une requête arrivée au serveur — un 404 n'est pas une erreur. Un échec asynchrone dit sa raison dans ue_failed ; un en-tête refusé à la pose (of_set_header) y dit aussi pourquoi, et elle est vidée quand une requête asynchrone part |
of_set_header (string as_name, string as_value) → long | Un en-tête envoyé avec chaque requête désormais ; un nom déjà posé est remplacé, une valeur vide retire l'en-tête. Content-Type est ajouté seul quand un corps part sans lui (JSON, UTF-8) ; pour un téléversement, celui posé ici est ignoré (multipart/form-data est posé par la bibliothèque). Aucun de ces en-têtes ne suit une redirection vers un autre hôte. Rend 0, ou -5 — rien ne change — pour un nom vide, un nom qui contient une espace, un deux-points ou un caractère de contrôle, ou une valeur qui contient un saut de ligne ; « un autre hôte » veut dire une autre origine : un autre hôte, un autre port ou un autre schéma — une URL http:// redirigée vers https:// perd son jeton. Une valeur part en Latin-1 : un caractère au-delà (grec, japonais…) est refusé (-5) dès la pose, of_get_last_error() dit pourquoi. Le User-Agent (PBToolboxAI/<version>) se remplace ici |
of_remove_header (string as_name) | Oublie un en-tête, par nom (la casse est indifférente) |
of_clear_headers ( ) | Oublie tous les en-têtes, l'authentification comprise |
of_set_bearer (string as_token) → long | Authorization: Bearer sur chaque requête. Une seule authentification à la fois : il remplace une authentification Basic ; un jeton vide retire l'authentification. Rend 0, ou -5 pour un jeton qui contient un saut de ligne — aucune authentification ne reste alors, jamais une moitié ; il remplace aussi un jeton OAuth (of_oauth_client_credentials) |
of_set_basic (string as_user, string as_password) → long | Authentification Basic ; la DLL encode elle-même le base64 — PowerBuilder 10 n'en a pas. Elle remplace un jeton Bearer ; un utilisateur vide retire l'authentification. Rend 0, ou -5 pour un utilisateur ou un mot de passe qui contient un saut de ligne — aucune authentification ne reste alors. Tout autre caractère est permis, accents et japonais compris : la paire part en base64 de son UTF-8. Le base64 ne protège rien : en https:// le TLS cache le mot de passe, en http:// il part lisible |
of_oauth_client_credentials (string as_token_url, string as_client, string as_secret, string as_scope) → long | OAuth2 « client credentials » : demande maintenant un jeton au point de jeton — un POST en formulaire de grant_type, client_id, client_secret et scope (vide = aucun) —, puis l'envoie en Authorization: Bearer sur chaque requête. Le jeton est redemandé de lui-même une minute avant son expiration (expires_in) et après un 401 : une requête synchrone est alors rejouée une fois, une asynchrone reçoit son 401 et la suivante renouvelle le jeton. La demande de jeton ne porte jamais l'authentification de l'objet. Une seule authentification à la fois : of_set_bearer, of_set_basic, of_clear_headers et of_reset l'arrêtent. Rend 0 une fois le jeton posé, -5 pour une URL ou un client vides, -4 quand le point de jeton refuse ou ne répond pas — of_get_last_error() dit pourquoi, avec l'error_description du serveur —, -2 si le client n'a pas pu être créé. Un échec garde les réglages : la requête suivante redemande |
of_get (string as_url) → long | GET. Rend le statut HTTP, ou -5 URL ou en-tête invalide, certificat client introuvable, -4 échec, -6 refusé en démo. Le fragment #… d'une URL n'est jamais envoyé au serveur. -5 aussi pour des identifiants écrits dans l'URL (https://user:mdp@…) — utilisez of_set_basic —, -2 si le client n'a pas pu être créé. La requête de l'URL part telle qu'écrite : construisez-la avec of_url dès qu'une valeur vient d'une saisie |
of_post (string as_url, string as_body) → long | POST avec un corps (JSON par défaut, construit avec n_pbt_json). Rend le statut HTTP, ou -5 / -4 / -6 |
of_post_form (string as_url, string as_names[], string as_values[]) → long | POST d'un formulaire HTML (application/x-www-form-urlencoded), chaque nom et chaque valeur encodés : ce qu'attend un point de jeton OAuth2, une API ancienne ou un formulaire web. Un nom vide est sauté, une valeur manquante est vide. Rend le statut HTTP, ou -5 / -4 / -6 / -2 comme of_post |
of_put (string as_url, string as_body) → long | PUT avec un corps. Rend le statut HTTP, ou -5 / -4 / -6 |
of_patch (string as_url, string as_body) → long | PATCH avec un corps. Rend le statut HTTP, ou -5 / -4 / -6 |
of_delete (string as_url) → long | DELETE. Rend le statut HTTP, ou -5 / -4 / -6 |
of_request (string as_method, string as_url, string as_body {, string as_headers}) → long | N'importe quelle méthode (METHOD_*, ou la vôtre), n'importe quel corps envoyé en UTF-8 : ce que les cinq raccourcis appellent. Rend le statut HTTP, ou -5 / -4 / -6. as_headers : des en-têtes pour cette requête seule — des lignes Nom: valeur jointes par ~r~n (un Idempotency-Key, un If-Match, un autre jeton) ; ils remplacent un en-tête de même nom, une valeur vide en écarte un, rien n'est gardé pour la requête suivante ; une ligne invalide rend -5 et rien ne part. Une méthode mal formée (GE T) rend -5 |
of_download (string as_url, string as_path) → long | GET directement dans un fichier, octets intacts, quelle que soit la taille. Rend le statut HTTP, ou -5 URL ou chemin vide, -4 échec, -6 refusé en démo. Un chemin relatif est résolu à l'appel dans le dossier courant de l'application au chargement de la bibliothèque — jamais là où un DirList ou une boîte de fichiers l'a déplacé depuis |
of_retrieve (datastore ads_target, string as_url {, string as_path}) → long | GET d'un corps JSON et le tableau d'objets trouvé à as_path dans un DataStore ("" = le corps est le tableau, "data/items" = un tableau à l'intérieur) — comme Retrieve : le DataStore est vidé d'abord, puis une ligne par objet, chaque membre dans la colonne du même nom (casse ignorée), converti à son type (n_pbt_json.of_to_datastore) ; les lignes arrivent non modifiées. Les mêmes surcharges existent pour un datawindow et un datawindowchild (une liste déroulante remplie depuis une API). Rend le nombre de lignes, ou -4 quand la réponse n'est pas un 2xx, n'est pas du JSON — en mode démo un corps est coupé à 4096 caractères — ou n'a pas de tableau à cet endroit (le DataStore garde alors ses lignes), -5 pour un DataStore ou une URL invalides, et les codes de of_get ; of_get_last_error() dit pourquoi |
of_response_text ( ) → string | Le corps de la dernière réponse synchrone de cet objet — un autre client ne l'écrase pas —, en texte : décodé selon le charset annoncé par le serveur (UTF-8, et les jeux historiques : windows-125x, iso-8859-x, shift_jis, gbk/gb2312, big5, euc-kr, koi8-r…), UTF-8 par défaut (un octet invalide devient un caractère de remplacement). Vide après un échec, et après of_download : ce corps-là est dans le fichier. Un corps de texte est plafonné à 64 Mo, 16 Mo dans un processus 32 bits : au-delà, -4 « body too large » — utilisez of_download |
of_response_headers ( ) → string | Tous les en-têtes de la dernière réponse synchrone, un par ligne |
of_response_header (string as_name) → string | Un en-tête de la dernière réponse synchrone, par nom ; vide s'il n'a pas été envoyé ; un en-tête envoyé plusieurs fois rend toutes ses valeurs, jointes par , — Set-Cookie par un saut de ligne (une date de cookie contient une virgule) |
of_json_value (string as_path) → string | Une valeur du corps JSON par son chemin — clés jointes par /, positions de tableau à partir de 1, la notation de n_pbt_json : "customer", "json/customer", "items/1/qty". Une chaîne revient décodée, un nombre ou true/false tels qu'écrits ("4152"), null vide, un objet ou un tableau en JSON ; vide si le chemin n'existe pas. Pour plus d'une valeur, chargez of_response_text dans un n_pbt_json. Le corps n'est analysé qu'une fois par réponse : lire dix valeurs coûte une seule analyse |
of_url (string as_path, string as_names[], string as_values[]) → string | Une URL avec sa requête, chaque nom et chaque valeur encodés : avec q = "C# & R&D" et page = "2", of_url("/items", …) rend /items?q=C%23%20%26%20R%26D&page=2. Un chemin qui a déjà une requête reçoit les paires après un &, un fragment (#top) reste au bout, un nom vide est sauté. Rien n'est gardé : sûr entre deux requêtes asynchrones. La bibliothèque n'encode jamais la requête d'une URL que vous écrivez vous-même — un # la coupe, un & ajoute un paramètre, un + devient une espace |
of_url_encode (string as_text) → string | Une valeur rendue sûre pour une URL (RFC 3986, UTF-8) : lettres, chiffres, -, ., _ et ~ restent, tout le reste devient %XX — "C# & co" rend C%23%20%26%20co. Vide pour un texte vide ou NULL |
of_reset ( ) → long | Retour aux valeurs par défaut : plus d'en-tête, plus d'URL de base, le délai par défaut, aucune tentative, cookies gardés ; le client est fermé — ses cookies partent, ses requêtes en vol sont annulées sans événement — et la dernière réponse est oubliée. L'authentification Windows est coupée (ib_windows_auth = false) et le certificat client oublié (is_client_certificate = "") ; ib_check_revocation revient à false, un jeton OAuth est oublié et is_last_request vidé Rend 0 une fois fait. |
Exemples #
Lire une liste et la parcourir #
// Local variables
string ls_body, ls_id
n_pbt_json lnv_list
long ll_i
// One value : of_json_value("items/1/id"). The whole list : load the body into an
// n_pbt_json and walk it by path.
if lnv_rest.of_get(/*url*/ "/orders?status=open") = 200 then
ls_body = lnv_rest.of_response_text()
lnv_list.of_load(/*json*/ ls_body)
for ll_i = 1 to lnv_list.of_count(/*path*/ "items")
ls_id = lnv_list.of_get_string(/*path*/ "items/" + String(ll_i) + "/id")
next
end if
Télécharger un document #
// Local variables
long ll_status
// Download the invoice to a file, then open it
ll_status = lnv_rest.of_download(/*url*/ "/orders/4152/invoice.pdf", /*path*/ "C:\temp\invoice-4152.pdf")
if ll_status = 200 then
Run("C:\temp\invoice-4152.pdf")
elseif ll_status < 0 then
MessageBox("Download", lnv_rest.of_get_last_error())
end if
Une API qui répond en erreur #
// Local variables
n_pbt_json lnv_j
// The status decides : 200 or 201 = created, 400 = the request was refused (is_last_request
// tells what it asked for, its secrets masked), 401 = the token expired, below 0 = the network failed
lnv_j.of_set_number(/*path*/ "order", /*value*/ 4152)
choose case lnv_rest.of_post(/*url*/ "/shipments", /*body*/ lnv_j.of_text())
case 200, 201
case 400
MessageBox("API", lnv_rest.of_json_value(/*path*/ "message") + "~r~n~r~n" + lnv_rest.is_last_request)
case 401
MessageBox("API", "Token expired : " + lnv_rest.of_json_value(/*path*/ "message"))
case is < 0
MessageBox("Network", lnv_rest.of_get_last_error())
end choose
Une API d'intranet en authentification Windows #
// Local variables
n_pbt_restclient lnv_rest
long ll_status
// An IIS API of the intranet : the Windows session answers the server's challenge,
// nothing is typed. Off by default : switch it on for this client only.
lnv_rest = create n_pbt_restclient
lnv_rest.is_base_url = "https://erp.intranet.local/api"
lnv_rest.ib_windows_auth = true
// A header refused (a line break in the value) returns -5 and is not set
if lnv_rest.of_set_header(/*name*/ "X-Client", /*value*/ "PowerBuilder") < 0 then return
// Read an order : 401 when the server refused the Windows account
ll_status = lnv_rest.of_get(/*url*/ "/orders/4152")
if ll_status = 401 then
MessageBox("ERP", "Access denied")
elseif ll_status < 0 then
MessageBox("ERP", lnv_rest.of_get_last_error())
end if
// The client is no longer needed
destroy lnv_rest
Une recherche saisie par l'utilisateur #
// Local variables
string ls_names[], ls_values[]
long ll_status
// What the user typed may hold # & + : of_url encodes every name and every value
ls_names[1] = "q"
ls_values[1] = sle_search.text
ls_names[2] = "page"
ls_values[2] = "2"
ll_status = lnv_rest.of_get(/*url*/ lnv_rest.of_url(/*path*/ "/items", /*names*/ ls_names, /*values*/ ls_values))
Une API protégée par OAuth2 #
// Local variables
n_pbt_restclient lnv_api
// One client for an API protected by OAuth2 (Azure AD, Keycloak, Salesforce...) : the token
// is asked once, then renewed by the client itself a minute before it expires
lnv_api = create n_pbt_restclient
lnv_api.is_base_url = "https://api.example.com"
if lnv_api.of_oauth_client_credentials(/*token_url*/ "https://login.example.com/oauth2/token", /*client*/ "my-app", /*secret*/ ProfileString("app.ini", "api", "secret", ""), /*scope*/ "orders.read") < 0 then
MessageBox("API", lnv_api.of_get_last_error())
return
end if
// Every request now carries the token
lnv_api.of_get(/*url*/ "/orders/4152")
destroy lnv_api
Une liste dans un DataStore #
// Local variables
datastore lds_orders
long ll_rows
// The open orders of the API straight into a DataStore, as Retrieve does from a database :
// one row per object, each member in the column of its name, the rows not modified
lds_orders = create datastore
lds_orders.DataObject = "d_orders"
ll_rows = lnv_rest.of_retrieve(/*target*/ lds_orders, /*url*/ "/orders?status=open", /*path*/ "items")
if ll_rows < 0 then MessageBox("API", lnv_rest.of_get_last_error())
Asynchrone (sans figer l'interface) #
Pendant un appel synchrone, l'interface est figée jusqu'à la réponse : la fenêtre ne se redessine plus et ne répond plus aux clics. C'est sans conséquence pour un appel court ; pour tout appel qui peut durer — un serveur lent, un gros corps, un réseau incertain —, prenez l'API asynchrone : la requête part sur un fil de travail et sa réponse revient plus tard en événement. Synchrone et asynchrone partagent la même session : le cookie posé par une connexion synchrone sert aux appels asynchrones qui suivent, et inversement ; un appel synchrone n'attend jamais derrière les requêtes asynchrones du même objet.
Mise en place — rien à câbler : le composant livre la réponse tout seul dans ue_response. of_open / of_close ouvrent et ferment le client (facultatif : la première requête l'ouvre), of_clear_cookies oublie les cookies gardés d'une requête à l'autre. En interne, un pump du composant appelle of_process_events pour drainer les réponses — vous ne l'appelez jamais.
Envoyer — of_request_async(method, url, body), ou les raccourcis of_get_async, of_post_async, of_put_async, of_patch_async, of_delete_async, rendent aussitôt un id de requête — ou -5 tout de suite, sans aucun événement ensuite, pour une URL ou un en-tête invalide (of_get_last_error() dit pourquoi, par exemple « invalid URL: … »). Six requêtes au plus partent en même temps ; les suivantes attendent leur tour dans une file. of_download_async(url, path) télécharge vers un fichier et of_upload(url, field, path) téléverse un fichier en multipart/form-data — les deux avec progression. of_cancel(id) arrête une requête. Les tentatives (il_max_retries, il_retry_backoff_ms) rejouent sur 429/503/panne réseau.
Recevoir — ue_response(al_id, al_status) (lire le corps avec of_response_text(al_id) / of_status(al_id) DANS le gestionnaire), ue_failed(al_id, al_code, as_error), ue_progress(al_id, al_done, al_total). Un id ne vaut que pour l'objet qui a envoyé la requête. Une erreur d'exécution dans l'un de ces événements n'est pas avalée : elle remonte à l'événement SystemError de l'application comme toute erreur de script, et les réponses suivantes sont livrées quand même.
| Méthode | Rôle |
|---|---|
of_open ( ) → long | Ouvre le client : sa session (cookies partagés par les appels synchrones et asynchrones) et son canal d'événements natif (pas de WebView, pas de CORS). Facultatif : la première requête l'ouvre. Rend l'identifiant du client (> 0), ou -2 s'il n'a pas pu être créé (of_get_last_error() dit pourquoi) |
of_close ( ) → long | Ferme le client : ses cookies (la session) partent avec lui, et ses requêtes encore en vol sont annulées sans aucun événement — ni ue_response, ni ue_failed. La prochaine requête le rouvre. Fait pour vous par of_reset et à la destruction. La dernière réponse synchrone de l'objet est oubliée avec lui Rend 0 une fois fait. |
of_process_events ( ) | Draine les réponses asynchrones et lève ue_response / ue_failed / ue_progress. Publique UNIQUEMENT parce que le pump interne du composant l'appelle sur la boucle PowerBuilder, toutes les quelques millisecondes tant qu'une requête est en vol — vous ne l'appelez jamais, et vous ne câblez ni récepteur ni timer. L'appeler vous-même est sans danger : elle ne vide que ce qui attend déjà |
of_clear_cookies ( ) | Oublie les cookies gardés d'une requête à l'autre (une déconnexion). Garder ou non les cookies ensuite reste l'affaire de ib_keep_cookies |
of_get_async (string as_url) → long | GET asynchrone. Rend aussitôt un id de requête ; la réponse arrive dans ue_response(id, status). Rend -5 tout de suite — et aucun événement ne suit — sur une URL ou un en-tête invalide (of_get_last_error() dit pourquoi), -2 si le client ne peut pas s'ouvrir |
of_post_async (string as_url, string as_body) → long | POST asynchrone avec un corps (JSON par défaut, construit avec n_pbt_json). Rend aussitôt un id de requête ; la réponse arrive dans ue_response. Rend -5 / -2 comme of_get_async |
of_post_form_async (string as_url, string as_names[], string as_values[]) → long | Le même formulaire que of_post_form, envoyé sans attendre. Rend aussitôt un id de requête ; la réponse arrive dans ue_response. Rend -5 / -2 comme of_get_async |
of_put_async (string as_url, string as_body) → long | PUT asynchrone avec un corps. Rend aussitôt un id de requête ; la réponse arrive dans ue_response. Rend -5 / -2 comme of_get_async |
of_patch_async (string as_url, string as_body) → long | PATCH asynchrone avec un corps. Rend aussitôt un id de requête ; la réponse arrive dans ue_response. Rend -5 / -2 comme of_get_async |
of_delete_async (string as_url) → long | DELETE asynchrone. Rend aussitôt un id de requête ; la réponse arrive dans ue_response. Rend -5 / -2 comme of_get_async |
of_request_async (string as_method, string as_url, string as_body {, string as_headers}) → long | Envoi asynchrone avec la méthode HTTP de votre choix (of_get_async et ses frères l'appellent). Rend aussitôt un id de requête ; la réponse arrive dans ue_response, l'échec dans ue_failed. Six requêtes au plus en même temps, les suivantes attendent leur tour. Rend -2 si le client ne s'ouvre pas, -4 si la requête ne peut pas être mise en file, -5 tout de suite — sans événement — pour une URL ou un en-tête invalide. as_headers : des en-têtes pour cette requête seule, comme pour of_request |
of_download_async (string as_url, string as_path) → long | GET asynchrone directement dans un FICHIER, ue_progress en chemin. Rend un id de requête, -5 sur un chemin vide, -6 refusé sur-le-champ en mode démo, -2 si le client ne s'ouvre pas. Un chemin relatif est résolu à l'appel, comme pour of_download |
of_upload (string as_url, string as_field, string as_path) → long | Envoie un FICHIER en multipart/form-data (champ de formulaire as_field), en asynchrone, avec ue_progress — toujours un POST, avec ce seul champ. Rend un id de requête, -5 sur un chemin vide, -6 refusé sur-le-champ en mode démo, -2 si le client ne s'ouvre pas. Un fichier relatif est cherché dans le dossier courant, puis dans celui de l'application au chargement de la bibliothèque, puis à côté de l'exe. Le Content-Type posé par of_set_header est ignoré : multipart/form-data est posé par la bibliothèque |
of_cancel (long al_id) → long | Demande à une requête de s'arrêter ; elle se termine par ue_failed(id, -4, "cancelled"), tout de suite si elle attendait encore son tour. Rend 0, -5 si l'id est inconnu ou appartient à un autre objet — rien n'est annulé —, -4 s'il est trop tard : la réponse est arrivée et vient dans ue_response |
of_status (long al_id) → long | Le statut HTTP d'une réponse asynchrone, par id de requête — à lire DANS ue_response. Rend le statut (200, 404…) ; 0 tant que la requête court, pour une requête échouée (dans ue_failed), pour un id inconnu, déjà oublié ou d'un autre objet |
of_response_text (long al_id) → string | Le corps d'une réponse asynchrone, par id de requête — à lire DANS ue_response : la requête est oubliée au retour de l'événement ; vide tant qu'elle court ; vide aussi pour l'id d'un autre objet |
of_response_headers (long al_id) → string | Tous les en-têtes d'une réponse asynchrone, un par ligne |
of_response_header (long al_id, string as_name) → string | Un en-tête d'une réponse asynchrone, par nom (la casse est indifférente) ; les valeurs répétées jointes comme of_response_header(as_name) |
of_json_value (long al_id, string as_path) → string | Une valeur du corps JSON d'une réponse asynchrone, par chemin — la même notation que of_json_value(as_path). À lire dans ue_response ; lue avant la réponse (dans ue_progress), elle rend vide, et la réponse est quand même lue quand elle arrive |
| Événement | Rôle |
|---|---|
ue_response (long al_id, long al_status) | Une requête asynchrone a abouti : al_id est l'id rendu à l'envoi, al_status le statut HTTP. Lire le corps avec of_response_text(al_id) DANS le gestionnaire : la requête est oubliée à la sortie de l'événement. Il s'écrit sur l'objet client posé dans la fenêtre, ou dans un descendant (voir « Où écrire les événements »). Fermer depuis lui la fenêtre qui porte le client est permis : les réponses qui attendaient encore ne sont pas livrées |
ue_failed (long al_id, long al_code, string as_error) | Une requête asynchrone a échoué. al_code dit la nature : -4 la requête a échoué (pas de réseau, délai dépassé, TLS, corps trop gros, ou annulée par of_cancel — as_error vaut alors cancelled), -5 fichier invalide ou certificat client introuvable, -6 refusé en mode démo (une requête de plage Range) ; as_error dit pourquoi. of_status(al_id) y rend 0 |
ue_progress (long al_id, long al_done, long al_total) | Avancement d'une requête asynchrone : le corps de la réponse au fil de son arrivée (toute requête, un gros JSON compris), le fichier d'un téléchargement (of_download_async) ou d'un envoi (of_upload). al_done octets sur al_total (0 quand le serveur n'a pas annoncé la taille) ; le dernier, juste avant ue_response, dit al_done = al_total. Au-delà de 2 Go, les octets ne tiennent plus dans un long |
Où écrire les événements #
Un objet créé par create n_pbt_restclient sert très bien le synchrone — c'est ce que fait le démarrage rapide —, mais ses événements n'ont aucun script : PowerBuilder ne permet pas d'écrire ue_response sur un objet créé dans un script. Pour l'asynchrone, deux voies :
- l'objet posé dans la fenêtre (ou l'objet utilisateur) : dans le peintre, insérez un objet non visuel
n_pbt_restclientet nommez-leinv_rest; ses événementsue_response,ue_failedetue_progressse scriptent alors comme ceux d'un contrôle, et il naît et meurt avec la fenêtre — c'est ce que fait l'application de démonstration ; - un descendant : héritez de
n_pbt_restclient(n_my_api), écrivezue_responsedans ce descendant, et créez-le parcreate n_my_api.
Fermer la fenêtre qui porte le client depuis l'un de ses événements est permis : les réponses qui attendaient encore ne sont pas livrées.
// Local variables
long ll_id
// Async : the UI never freezes, and there is nothing to wire. inv_rest is the restclient
// placed in the window ; the request id comes back at once
ll_id = inv_rest.of_get_async(/*url*/ "https://api.example.com/orders/4152")
// Local variables
string ls_body
// Event ue_response of inv_rest : the answer arrives on its own, and it is read here
if al_status >= 200 and al_status < 300 then ls_body = this.of_response_text(/*id*/ al_id)
Bonnes pratiques #
- Un client par API, avec son
is_base_urlet ses en-têtes posés une fois : le code des appels ne porte plus que le chemin et le corps. - Le corps se construit avec
n_pbt_json, jamais par concaténation : un guillemet dans un nom de client ne casse rien. - Testez le statut, puis le code négatif :
>= 200 et < 300c'est bon,>= 400le serveur a répondu non,< 0la requête n'est pas partie —of_get_last_error()dit pourquoi. - Le délai : 30 secondes par défaut ; un rapport lourd en mérite plus, un contrôle de présence moins.
- Ce qui peut durer part en asynchrone : un appel synchrone fige l'interface jusqu'à la réponse.
- Une redirection garde votre requête : une 301 ou une 302 ne réécrit en GET qu'un POST — un PUT, un PATCH ou un DELETE est refait tel quel, corps compris ; une 303 repart en GET sans corps. Une
Locationrelative (?page=2,../) est résolue comme un navigateur le ferait. - Une redirection vers une autre origine n'emporte rien de vous : aucun des en-têtes posés par
of_set_header(jeton, clé d'API) ne la suit — seulContent-Typereste. Une origine, c'est un hôte, un port et un schéma : une APIhttp://redirigée vershttps://perd son jeton, écrivez donchttps://dansis_base_url. Les identifiants Windows (ib_windows_auth) et le certificat client ne sont donnés qu'à l'hôte et au port de la requête. Une redirection HTTPS → HTTP est refusée. - Testez le retour de
of_set_header,of_set_beareretof_set_basicquand la valeur vient d'une saisie ou d'un fichier : un saut de ligne, ou un caractère au-delà du Latin-1 (un en-tête voyage en Latin-1 ; pas pourof_set_basic, dont la paire part en base64), y est refusé (-5) et rien n'est posé. Un en-tête refusé ne part jamais à moitié. - Les identifiants Windows ne partent que si vous le demandez :
ib_windows_auth = truepour une API d'intranet en authentification Windows, jamais sur un client qui parle à Internet. - Un corps de texte est plafonné à 64 Mo, 16 Mo dans une application 32 bits : un export volumineux se lit avec
of_download/of_download_async, qui écrivent un fichier sans limite. - Encodez ce que l'utilisateur a tapé : la requête d'une URL part telle qu'écrite — un
#la coupe, un&ajoute un paramètre, un+devient une espace. Construisez-la avecof_url, ou encodez une valeur avecof_url_encode. - Jamais d'identifiants dans l'URL (
https://user:mdp@…) : la requête est refusée (-5) ; prenezof_set_basic. - Le
User-AgentvautPBToolboxAI/<version>;of_set_header("User-Agent", …)le remplace quand une API exige le vôtre.