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
long ll_status
// One client for the API : its address and the token it sends
lnv_rest = create n_pbt_restclient
lnv_rest.is_base_url = "https://api.example.com"
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.is_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 ; is_last_error dit lequel), -6 refusé en mode démo.
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 » |
is_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 |
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 |
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 |
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_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 |
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é |
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 |
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 |
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_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) → 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 |
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_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é |
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_reset ( ) | 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 = "") |
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.is_last_error)
end if
Une API qui répond en erreur #
// The status decides : 200 or 201 = created, 401 = the token expired,
// below 0 = the network failed
choose case lnv_rest.of_post(/*url*/ "/shipments", /*body*/ ls_body)
case 200, 201
case 401
MessageBox("API", "Token expired : " + lnv_rest.of_json_value(/*path*/ "message"))
case is < 0
MessageBox("Network", lnv_rest.is_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.is_last_error)
end if
// The client is no longer needed
destroy lnv_rest
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 (is_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éé (is_last_error dit pourquoi) |
of_close ( ) | 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 |
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 (is_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_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) → 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 |
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) |
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) |
| É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 |
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'un téléchargement (of_download_async) ou d'un envoi (of_upload) asynchrone : al_done octets sur al_total (0 quand le serveur n'a pas annoncé la taille). Au-delà de 2 Go, les octets ne tiennent plus dans un long |
// Async : the UI never freezes, and there is nothing to wire.
ll_id = uo_rest.of_get_async(/*url*/ "https://api.example.com/orders/4152")
// The answer arrives on its own in the ue_response event of uo_rest :
IF al_status >= 200 AND al_status < 300 THEN
ls_body = uo_rest.of_response_text(/*id*/ al_id)
END IF
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 —is_last_errordit 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 un autre hôte 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 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 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.