PBToolboxAI v4 ← Site

restclient — n_pbt_restclient #

← Component reference · Guide contents

An HTTPS client for PowerBuilder, version 10 included: GET, POST, PUT, PATCH, DELETE, headers, a JSON body, a file download. Native — the system's TLS, proxy and decompression, and no CORS wall, because the request does not leave from a page.

▶ See it live — Demo application, REST client tile: the answers, the code that gets them and this page, side by side (Internet connection required).


At a glance #

Nonvisual objectn_pbt_restclient
Used forCalling a REST API from a PowerBuilder application: read, create, update, download
PrincipleA request is a call that returns the HTTP status; the answer waits in the object, read by of_response_text or of_json_value
DependencyWinHTTP, inside the library's DLL — no page, no extra runtime

Quick start #

// 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

Every HTTP method returns the status the server chose — a 404 is an answer, not an error — or a negative code when the request did not go through: -5 invalid URL or header, client certificate not found, -4 failure (no network, unknown name, timeout, TLS, a text body over 64 MB — 16 MB in a 32-bit process; is_last_error says which), -6 refused in demo mode.


Why native, and not the page #

A fetch() launched from a page of the library is subject to CORS: every API that does not send Access-Control-Allow-Origin is closed to it, and enterprise APIs do not send it. So the request leaves from the DLL, through WinHTTP: the system's TLS and certificates, the configured proxy, gzip. What you lose: nothing; what you gain: every API.


Properties #

PropertyTypeDefaultRole
is_base_urlstring""Put in front of a relative URL: is_base_url = "https://api.example.com" then of_get("/orders/4152"). An absolute URL is used as it is
il_timeout_mslong30000How long each step of a request may take: resolving the name, connecting, sending, then waiting for each block of the answer. A large download that keeps moving is never cut; a server silent for that long is: -4 and "timed out". 0 or less means the default, 30 s: there is no "no timeout"
is_last_errorstring""Why the last synchronous request — or an async call refused at once — returned a negative code. Empty after a request that reached the server — a 404 is not an error. An async failure says why in ue_failed
il_statuslong0The HTTP status of the last synchronous request, or its negative code; the value the request returned. An async answer has its own: of_status(id)
ib_keep_cookiesbooleantrueKeeps the cookies between requests — synchronous and async ones, which share one session: an API that sets a cookie on login keeps you logged in. At false, no cookie is sent or kept any more, and the ones kept so far are forgotten; applied at once
il_max_retrieslong0How many times an async request is tried again on 429, 503 or a network failure. A POST whose answer was lost is never tried again: it may have been processed. A failure that would happen again identically is never replayed: too many redirections, an invalid redirection, an HTTPS → HTTP redirection refused, a TLS failure, a file that cannot be written
il_retry_backoff_mslong500The wait before the first retry, in milliseconds, doubled at each try and capped at 60 s. A Retry-After header from the server (seconds or a date) takes precedence
ib_windows_authbooleanfalseIntegrated Windows authentication (Negotiate / NTLM): when a server asks for it — an intranet IIS API —, the credentials of the Windows session answer, with nothing typed. Off by default: without it they are never sent, to any server. Read at every request
is_client_certificatestring""A client certificate (mutual TLS): the SHA-1 thumbprint of a certificate of the personal store — the user's, then the machine's —, as the certificate manager shows it; spaces and colons ignored. Empty = none. A thumbprint not found makes every request return -5 ("client certificate not found"). Read at every request. There is no option to ignore a server certificate error
ipo_ownerpowerobjectnullThe visual object this client works for: the licence is checked on its class. Needed only in the demonstration application; a development or runtime key unlocks the client without it. Not unlocked, the client runs in demo mode — bodies cut at 4096 characters, no download and no upload

Constants: METHOD_GET, METHOD_POST, METHOD_PUT, METHOD_PATCH, METHOD_DELETE for of_request.


Methods #

MethodRole
of_set_header (string as_name, string as_value) → longA header sent with every request from now on; a name already set is replaced, an empty value removes the header. Content-Type is added by itself when a body leaves without one (JSON, UTF-8); for an upload, the one set here is ignored (multipart/form-data is set by the library). None of these headers follows a redirection to another host. Returns 0, or -5 — nothing changes — for an empty name, a name holding a space, a colon or a control character, or a value holding a line break
of_remove_header (string as_name)Forgets one header, by name (case does not matter)
of_clear_headers ( )Forgets every header, the authentication included
of_set_bearer (string as_token) → longAuthorization: Bearer on every request. One authentication at a time: it replaces a Basic one; an empty token removes the authentication. Returns 0, or -5 for a token holding a line break — no authentication is then left, never half of one
of_set_basic (string as_user, string as_password) → longBasic authentication; the DLL encodes the base64 itself — PowerBuilder 10 has none. It replaces a bearer token; an empty user removes the authentication. Returns 0, or -5 for a user or a password holding a line break — no authentication is then left
of_get (string as_url) → longGET. Returns the HTTP status, or -5 invalid URL or header, client certificate not found, -4 failure, -6 refused in demo. The #… fragment of a URL is never sent to the server
of_post (string as_url, string as_body) → longPOST with a body (JSON by default, built with n_pbt_json). Returns the HTTP status, or -5 / -4 / -6
of_put (string as_url, string as_body) → longPUT with a body. Returns the HTTP status, or -5 / -4 / -6
of_patch (string as_url, string as_body) → longPATCH with a body. Returns the HTTP status, or -5 / -4 / -6
of_delete (string as_url) → longDELETE. Returns the HTTP status, or -5 / -4 / -6
of_request (string as_method, string as_url, string as_body) → longAny method (METHOD_*, or your own), any body sent as UTF-8: what the five shortcuts call. Returns the HTTP status, or -5 / -4 / -6
of_download (string as_url, string as_path) → longGET straight into a file, bytes untouched, whatever the size. Returns the HTTP status, or -5 invalid URL or empty path, -4 failure, -6 refused in demo. A relative path is resolved at the call in the application's current folder when the library was loaded — never where a DirList or a file dialog has moved it since
of_response_text ( ) → stringThe body of the last synchronous response of this object — another client does not overwrite it —, as text: decoded from the charset the server announced (UTF-8, and the legacy sets: windows-125x, iso-8859-x, shift_jis, gbk/gb2312, big5, euc-kr, koi8-r…), UTF-8 by default (an invalid byte becomes a replacement character). Empty after a failure, and after of_download: that body is in the file. A text body is capped at 64 MB, 16 MB in a 32-bit process: beyond, -4 "body too large" — use of_download
of_response_headers ( ) → stringEvery header of the last synchronous answer, one per line
of_response_header (string as_name) → stringOne header of the last synchronous answer, by name; empty when it was not sent
of_json_value (string as_path) → stringOne value of the JSON body by its path — keys joined by /, array positions from 1, the n_pbt_json notation: "customer", "json/customer", "items/1/qty". A string comes back decoded, a number or true/false as written ("4152"), null empty, an object or an array as JSON; empty when the path does not exist. For more than one value, load of_response_text into an n_pbt_json. The body is parsed once per response: reading ten values costs a single parse
of_reset ( )Back to the defaults: no header, no base URL, the default timeout, no retry, cookies kept; the client is closed — its cookies go, its requests in flight are cancelled without any event — and the last answer is forgotten. Windows authentication is switched off (ib_windows_auth = false) and the client certificate forgotten (is_client_certificate = "")

Examples #

Reading a list and walking it #

// 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

Downloading a 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

An API that answers with an error #

// 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

An intranet API with Windows authentication #

// 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

Asynchronous (without freezing the UI) #

During a synchronous call the UI is frozen until the answer: the window no longer repaints and no longer answers clicks. That does not matter for a short call; for any call that may take time — a slow server, a large body, an uncertain network —, use the asynchronous API: the request runs on a worker thread and its answer comes back later as an event. Synchronous and async calls share the same session: the cookie set by a synchronous login serves the async calls that follow, and the other way round; a synchronous call never waits behind the async requests of the same object.

Setup — nothing to wire: the component delivers the answer on its own in ue_response. of_open / of_close open and close the client (optional: the first request opens it), of_clear_cookies forgets the cookies kept between requests. Internally the component's pump calls of_process_events to drain the answers — you never call it.

Send — of_request_async(method, url, body), or the shortcuts of_get_async, of_post_async, of_put_async, of_patch_async, of_delete_async, return a request id at once — or -5 at once, with no event afterwards, for an invalid URL or header (is_last_error says why, for instance "invalid URL: …"). Six requests at most run at the same time; the next ones wait their turn in a queue. of_download_async(url, path) downloads to a file and of_upload(url, field, path) uploads a file as multipart/form-data — both with progress. of_cancel(id) stops a request. Retries (il_max_retries, il_retry_backoff_ms) replay on 429/503/network failure.

Receive — ue_response(al_id, al_status) (read the body with of_response_text(al_id) / of_status(al_id) INSIDE the handler), ue_failed(al_id, al_code, as_error), ue_progress(al_id, al_done, al_total). An id is valid only for the object that sent the request. A runtime error in one of these events is not swallowed: it reaches the application's SystemError event like any script error, and the next answers are delivered all the same.

MethodRole
of_open ( ) → longOpens the client: its session (cookies shared by the synchronous and async calls) and its native event channel (no WebView, no CORS). Optional: the first request opens it. Returns the client id (> 0), or -2 when it could not be created (is_last_error says why)
of_close ( )Closes the client: its cookies (the session) go with it, and its requests still in flight are cancelled without any event — no ue_response, no ue_failed. The next request reopens it. Done for you by of_reset and on destroy. The object's last synchronous response is forgotten with it
of_process_events ( )Drains the async answers and raises ue_response / ue_failed / ue_progress. Public ONLY because the component's own pump calls it on the PowerBuilder loop, every few milliseconds while a request is in flight — you never call it, and you wire neither a receiver nor a timer. Calling it yourself is harmless: it only empties what is already waiting
of_clear_cookies ( )Forgets the cookies kept between requests (a logout). Whether cookies are kept from then on is still ib_keep_cookies
of_get_async (string as_url) → longAsync GET. Returns a request id at once; the answer arrives in ue_response(id, status). Returns -5 at once — and no event follows — for an invalid URL or header (is_last_error says why), -2 if the client cannot open
of_post_async (string as_url, string as_body) → longAsync POST with a body (JSON by default, built with n_pbt_json). Returns a request id at once; the answer comes in ue_response. Returns -5 / -2 like of_get_async
of_put_async (string as_url, string as_body) → longAsync PUT with a body. Returns a request id at once; the answer comes in ue_response. Returns -5 / -2 like of_get_async
of_patch_async (string as_url, string as_body) → longAsync PATCH with a body. Returns a request id at once; the answer comes in ue_response. Returns -5 / -2 like of_get_async
of_delete_async (string as_url) → longAsync DELETE. Returns a request id at once; the answer comes in ue_response. Returns -5 / -2 like of_get_async
of_request_async (string as_method, string as_url, string as_body) → longAsync request with the HTTP method of your choice (of_get_async and its siblings call it). Returns a request id at once; the answer comes in ue_response, a failure in ue_failed. Six requests at most at the same time, the next ones wait their turn. Returns -2 when the client cannot open, -4 when the request cannot be queued, -5 at once — with no event — for an invalid URL or header
of_download_async (string as_url, string as_path) → longAsync GET straight to a FILE, with ue_progress along the way. Returns a request id, -5 on an empty path, -6 refused at once in demo mode, -2 when the client cannot open. A relative path is resolved at the call, as for of_download
of_upload (string as_url, string as_field, string as_path) → longUploads a FILE as multipart/form-data (form field as_field), async, with ue_progress — always a POST, with that single field. Returns a request id, -5 on an empty path, -6 refused at once in demo mode, -2 when the client cannot open. A relative file is looked for in the current folder, then in the application's folder when the library was loaded, then next to the exe. The Content-Type set by of_set_header is ignored: multipart/form-data is set by the library
of_cancel (long al_id) → longAsks a request to stop; it ends with ue_failed(id, -4, "cancelled"), at once if it was still waiting its turn. Returns 0, -5 if the id is unknown or belongs to another object — nothing is cancelled —, -4 if it is too late: the answer has arrived and comes in ue_response
of_status (long al_id) → longThe HTTP status of an async answer, by request id — read it INSIDE ue_response. Returns the status (200, 404…); 0 while the request runs, for a failed request (in ue_failed), for an id unknown, already forgotten or of another object
of_response_text (long al_id) → stringThe body of an async answer, by request id — read it INSIDE ue_response: the request is forgotten once the event returns; empty while it is running; empty too for the id of another object
of_response_headers (long al_id) → stringEvery header of an async answer, one per line
of_response_header (long al_id, string as_name) → stringOne header of an async answer, by name (case does not matter)
of_json_value (long al_id, string as_path) → stringOne value of an async answer's JSON body, by path — the same notation as of_json_value(as_path)
EventRole
ue_response (long al_id, long al_status)An async request finished: al_id is the id returned when sending, al_status the HTTP status. Read the body with of_response_text(al_id) INSIDE the handler: the request is forgotten once the event returns
ue_failed (long al_id, long al_code, string as_error)An async request failed. al_code says the kind: -4 the request failed (no network, timeout, TLS, body too large, or cancelled by of_cancel — as_error is then cancelled), -5 invalid file or client certificate not found, -6 refused in demo mode (a Range request); as_error says why. of_status(al_id) returns 0 there
ue_progress (long al_id, long al_done, long al_total)Progress of an async download (of_download_async) or upload (of_upload): al_done bytes out of al_total (0 when the server did not announce the size). Past 2 GB the byte counts no longer fit a 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

Good practice #

← Component reference · Guide contents