PBToolboxAI v4 ← Site

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 visueln_pbt_restclient
Sert àAppeler une API REST depuis une application PowerBuilder : lire, créer, mettre à jour, télécharger
PrincipeUne 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épendanceWinHTTP, 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éTypeDéfautRôle
is_base_urlstring""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_mslong30000Dé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_errorstring""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_statuslong0Le 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_cookiesbooleantrueGarde 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_retrieslong0Combien 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_mslong500L'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_authbooleanfalseAuthentification 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_certificatestring""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_ownerpowerobjectnullL'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éthodeRôle
of_set_header (string as_name, string as_value) → longUn 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) → longAuthorization: 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) → longAuthentification 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) → longGET. 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) → longPOST 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) → longPUT avec un corps. Rend le statut HTTP, ou -5 / -4 / -6
of_patch (string as_url, string as_body) → longPATCH avec un corps. Rend le statut HTTP, ou -5 / -4 / -6
of_delete (string as_url) → longDELETE. Rend le statut HTTP, ou -5 / -4 / -6
of_request (string as_method, string as_url, string as_body) → longN'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) → longGET 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 ( ) → stringLe 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 ( ) → stringTous les en-têtes de la dernière réponse synchrone, un par ligne
of_response_header (string as_name) → stringUn 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) → stringUne 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éthodeRôle
of_open ( ) → longOuvre 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) → longGET 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) → longPOST 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) → longPUT 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) → longPATCH 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) → longDELETE 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) → longEnvoi 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) → longGET 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) → longEnvoie 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) → longDemande à 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) → longLe 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) → stringLe 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) → stringTous les en-têtes d'une réponse asynchrone, un par ligne
of_response_header (long al_id, string as_name) → stringUn en-tête d'une réponse asynchrone, par nom (la casse est indifférente)
of_json_value (long al_id, string as_path) → stringUne valeur du corps JSON d'une réponse asynchrone, par chemin — la même notation que of_json_value(as_path)
ÉvénementRô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 #

← Référence des composants · Sommaire du guide