Skip to content

Commit 2f95da3

Browse files
authored
RESTMessageV2 - GET with backoff - Pull 1 (#2368)
* Add README for RESTMessageV2 GET with backoff and telemetry This README explains how to use the RESTMessageV2 GET helper for handling API throttling, telemetry, and pagination. * Implement RestGetWithBackoff class for REST API calls This script includes a class for performing REST GET requests with retry handling, exponential backoff, and pagination support. It provides methods to fetch all pages of results and handle errors gracefully. * Add example usage for RestGetWithBackoff Added a background script example for using RestGetWithBackoff.
1 parent d7cea95 commit 2f95da3

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# RESTMessageV2 GET with backoff, telemetry, and simple pagination
2+
3+
## What this solves
4+
External APIs frequently throttle with HTTP 429 or intermittently return 5xx. This helper retries safely, honours Retry-After, logs simple telemetry, and follows a links.next pagination model.
5+
6+
## Where to use
7+
Script Include can be called from Scheduled Jobs, Flow Actions, Business Rules, or Background Scripts.
8+
9+
## How it works
10+
- Executes RESTMessageV2 requests
11+
- On 429 or 5xx, sleeps using Retry-After or exponential backoff
12+
- Collects minimal telemetry about attempts and total sleep time
13+
- Appends items from json.items and follows json.links.next
14+
15+
## References
16+
- RESTMessageV2 API
17+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/concept/c_RESTMessageV2API.html
18+
- Direct RESTMessageV2 example
19+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/reference/r_DirectRESTMessageV2Example.html
20+
- Inbound rate limiting and Retry-After header
21+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/inbound-REST-API-rate-limiting.html
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Script Include: RestGetWithBackoff
3+
* Purpose: Safely perform RESTMessageV2 GET requests with retry handling,
4+
* exponential backoff, and simple pagination support.
5+
*
6+
* Example usage (Background Script):
7+
* var helper = new RestGetWithBackoff();
8+
* var data = helper.getAll({
9+
* endpoint: 'https://api.example.com/v1/items',
10+
* headers: { 'Authorization': 'Bearer ${token}' },
11+
* maxRetries: 4,
12+
* baseDelayMs: 750
13+
* });
14+
* gs.info('Fetched ' + data.length + ' records');
15+
*/
16+
17+
var RestGetWithBackoff = Class.create();
18+
RestGetWithBackoff.prototype = {
19+
initialize: function() {},
20+
21+
/**
22+
* Main entry point to fetch all pages of results.
23+
* Handles retries, pagination, and aggregates results.
24+
* @param {Object} options - endpoint, headers, maxRetries, baseDelayMs
25+
* @returns {Array} all items combined from paginated responses
26+
*/
27+
getAll: function(options) {
28+
var url = options.endpoint; // Initial API endpoint
29+
var headers = options.headers || {}; // Optional request headers
30+
var maxRetries = options.maxRetries || 5; // Maximum retry attempts per page
31+
var baseDelayMs = options.baseDelayMs || 500;// Base delay for exponential backoff
32+
33+
var items = []; // Array to collect all items across pages
34+
var attempts = 0; // Total number of REST calls
35+
var totalSleepMs = 0; // Total delay time across retries
36+
37+
// Continue fetching until there are no more pages (links.next = null)
38+
while (url) {
39+
// Execute the REST call (with internal retry logic)
40+
var res = this._execute('get', url, headers, maxRetries, baseDelayMs);
41+
attempts += res.attempts; // Count total attempts made
42+
totalSleepMs += res.sleptMs; // Sum total sleep time used in retries
43+
44+
// If non-success HTTP code, throw to stop execution
45+
if (res.status < 200 || res.status >= 300)
46+
throw 'HTTP ' + res.status + ' for ' + url + ': ' + res.body;
47+
48+
// Parse and validate JSON body
49+
var json = this._safeJson(res.body);
50+
51+
// If body contains an 'items' array, append to results
52+
if (Array.isArray(json.items)) items = items.concat(json.items);
53+
54+
// Get next page link if available (standard 'links.next' pattern)
55+
url = json && json.links && json.links.next ? json.links.next : null;
56+
}
57+
58+
// Log a completion summary
59+
gs.info('REST helper complete. items=' + items.length +
60+
', attempts=' + attempts +
61+
', sleptMs=' + totalSleepMs);
62+
63+
return items;
64+
},
65+
66+
/**
67+
* Executes a REST call with retry and exponential backoff.
68+
* Retries on HTTP 429 (Too Many Requests) or 5xx errors.
69+
* @returns {Object} status, body, attempts, sleptMs
70+
*/
71+
_execute: function(method, url, headers, maxRetries, baseDelayMs) {
72+
var attempt = 0;
73+
var sleptMs = 0;
74+
75+
while (true) {
76+
attempt++;
77+
78+
// Build the RESTMessageV2 object
79+
var r = new sn_ws.RESTMessageV2();
80+
r.setEndpoint(url);
81+
r.setHttpMethod(method.toUpperCase());
82+
83+
// Apply custom headers (for example, auth tokens or content type)
84+
Object.keys(headers).forEach(function(k) { r.setRequestHeader(k, headers[k]); });
85+
86+
// Execute the request
87+
var resp = r.execute();
88+
var status = resp.getStatusCode();
89+
var body = resp.getBody();
90+
91+
// Success range (2xx)
92+
if (status >= 200 && status < 300) {
93+
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
94+
}
95+
96+
// Handle 429 (rate limit) or transient 5xx server errors
97+
if (status === 429 || status >= 500) {
98+
// Stop retrying if max reached
99+
if (attempt >= maxRetries) {
100+
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
101+
}
102+
103+
// Honour Retry-After header if present; otherwise exponential delay
104+
var retryAfter = Number(resp.getHeader('Retry-After')) || 0;
105+
var delayMs = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * baseDelayMs;
106+
107+
// Log retry details for visibility in system logs
108+
gs.info('Retrying ' + url + ' after ' + delayMs +
109+
' ms due to HTTP ' + status + ' (attempt ' + attempt + ')');
110+
111+
gs.sleep(delayMs); // Wait before retrying
112+
sleptMs += delayMs;
113+
continue;
114+
}
115+
116+
// Non-retryable failure (e.g., 4xx not including 429)
117+
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
118+
}
119+
},
120+
121+
/**
122+
* Safe JSON parser that throws descriptive error on invalid JSON.
123+
* @param {String} body - raw HTTP response text
124+
* @returns {Object} parsed JSON
125+
*/
126+
_safeJson: function(body) {
127+
try {
128+
return JSON.parse(body || '{}');
129+
} catch (e) {
130+
throw 'Invalid JSON: ' + e.message;
131+
}
132+
},
133+
134+
type: 'RestGetWithBackoff'
135+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Background Script usage example for RestGetWithBackoff
2+
(function() {
3+
var helper = new RestGetWithBackoff();
4+
var data = helper.getAll({
5+
endpoint: 'https://api.example.com/v1/things?limit=100',
6+
headers: { 'Authorization': 'Bearer ${token}' },
7+
maxRetries: 4,
8+
baseDelayMs: 750
9+
});
10+
gs.info('Fetched ' + data.length + ' items total');
11+
})();

0 commit comments

Comments
 (0)