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 object | n_pbt_restclient |
| Used for | Calling a REST API from a PowerBuilder application: read, create, update, download |
| Principle | A request is a call that returns the HTTP status; the answer waits in the object, read by of_response_text or of_json_value |
| Dependency | WinHTTP, inside the library's DLL — no page, no extra runtime |
Quick start #
n_pbt_restclient lnv_rest
n_pbt_json lnv_j
long ll_status
lnv_rest = create n_pbt_restclient
lnv_rest.is_base_url = "https://api.example.com"
lnv_rest.of_set_bearer(ls_token)
// Read : the status comes back, the body waits in the object
ll_status = lnv_rest.of_get("/orders/4152")
if ll_status = 200 then ls_customer = lnv_rest.of_json_value("customer")
// Create : the body is built with n_pbt_json, never by hand
lnv_j.of_set_number("order", 4152)
lnv_j.of_set_string("status", "shipped")
ll_status = lnv_rest.of_post("/shipments", lnv_j.of_text())
if ll_status < 0 then MessageBox("API", lnv_rest.is_last_error)
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, -4 failure (no network, unknown name, timeout, TLS; 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 #
| Property | Type | Default | Role |
|---|---|---|---|
is_base_url | string | "" | 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_ms | long | 30000 | How long a request may take, connection and answer included. Past it: -4 and "timed out" |
is_last_error | string | "" | Why the last request returned a negative code. Empty after a request that reached the server — a 404 is not an error |
il_status | long | 0 | The HTTP status of the last request, or its negative code; the value the request returned |
Constants: METHOD_GET, METHOD_POST, METHOD_PUT, METHOD_PATCH, METHOD_DELETE for of_request.
Methods #
| Method | Role |
|---|---|
of_set_header (string as_name, string as_value) | A header sent with every request from now on; a name already set is replaced. Content-Type is added by itself when a body is sent without one (JSON, UTF-8) |
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) | Authorization: Bearer on every request; an empty token removes it |
of_set_basic (string as_user, string as_password) | Basic authentication; the DLL encodes the base64 itself — PowerBuilder 10 has none |
of_get (string as_url) → long | GET. Returns the HTTP status, or -5 invalid URL, -4 failure, -6 refused in demo |
of_post (string as_url, string as_body) → long | POST 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) → long | PUT with a body. Returns the HTTP status, or -5 / -4 / -6 |
of_patch (string as_url, string as_body) → long | PATCH with a body. Returns the HTTP status, or -5 / -4 / -6 |
of_delete (string as_url) → long | DELETE. Returns the HTTP status, or -5 / -4 / -6 |
of_request (string as_method, string as_url, string as_body) → long | Any 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) → long | GET 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 |
of_response_text ( ) → string | The body of the last answer, as text (UTF-8 decoded) |
of_response_headers ( ) → string | Every header of the last answer, one per line |
of_response_header (string as_name) → string | One header of the last answer, by name; empty when it was not sent |
of_json_value (string as_key) → string | One value of the JSON body by its key, the first occurrence at any depth. For more than one value, read of_response_text and n_pbt_utils.of_json_get_* |
of_reset ( ) | Back to the defaults: no header, no base URL, the default timeout, the last answer forgotten |
Examples #
Reading a list and walking it #
string ls_body
if lnv_rest.of_get("/orders?status=open") = 200 then
ls_body = lnv_rest.of_response_text()
// One value : of_json_value. The whole list : n_pbt_utils.of_json_get_payload and
// of_json_get_str on each object, or your own parser on ls_body.
end if
Downloading a document #
long ll_status
ll_status = lnv_rest.of_download("/orders/4152/invoice.pdf", "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 #
choose case lnv_rest.of_post("/shipments", ls_body)
case 200, 201
// created
case 401
MessageBox("API", "Token expired : " + lnv_rest.of_json_value("message"))
case is < 0
MessageBox("Network", lnv_rest.is_last_error)
end choose
Asynchronous (no UI freeze) #
The synchronous calls above block the script (the UI stays alive but the line waits). The asynchronous API does not even block the script: the request runs on a worker thread and its answer comes back later as an event.
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 async call 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. 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 an in-flight 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, as_error), ue_progress(al_id, al_done, al_total).
| Method | Role |
|---|---|
of_open ( ) → long | Opens the async client, a native event channel (no WebView, no CORS). Optional: the first async call opens it. Returns the client id (> 0), or a negative code |
of_close ( ) | Closes the async client: its cookies (the session) go with it. The next async call reopens it. Done for you by of_reset and on destroy |
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). The ib_keep_cookies setting is unchanged |
of_get_async (string as_url) → long | Async GET. Returns a request id at once; the answer comes in ue_response(id, status). Returns -5 on an invalid URL, -2 when the client cannot open |
of_post_async (string as_url, string as_body) → long | Async 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) → long | Async 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) → long | Async 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) → long | Async 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) → long | Async 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. Returns -2 when the client cannot open |
of_download_async (string as_url, string as_path) → long | Async GET straight to a FILE, with ue_progress along the way. Returns a request id, -5 on an empty path, -2 when the client cannot open |
of_upload (string as_url, string as_field, string as_path) → long | Uploads a FILE as multipart/form-data (form field as_field), async, with ue_progress. Returns a request id, -5 on an empty path, -2 when the client cannot open, -6 in demo mode |
of_cancel (long al_id) → long | Asks an in-flight request to stop; it ends with ue_failed(id, "cancelled"). Returns 0, or -1 when the id is unknown |
of_status (long al_id) → long | The HTTP status of an async answer, by request id - read it INSIDE ue_response. Returns the status (200, 404...), 0 when the request is unknown or already forgotten |
| Event | Role |
|---|---|
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, string as_error) | An async request failed: no network, timeout, TLS, or cancelled by of_cancel (as_error is then cancelled) |
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 |
// Async : the UI never freezes, and there is nothing to wire.
ll_id = uo_rest.of_get_async("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(al_id)
END IF
Good practice #
- One client per API, with its
is_base_urland its headers set once: the call code carries only the path and the body. - The body is built with
n_pbt_json, never by concatenation: a quote in a customer name breaks nothing. - Test the status, then the negative code:
>= 200 and < 300is fine,>= 400the server said no,< 0the request did not leave —is_last_errorsays why. - The timeout: 30 seconds by default; a heavy report deserves more, a health check less.