Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# RESTMessageV2 GET with backoff, telemetry, and simple pagination

## What this solves
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.

## Where to use
Script Include can be called from Scheduled Jobs, Flow Actions, Business Rules, or Background Scripts.

## How it works
- Executes RESTMessageV2 requests
- On 429 or 5xx, sleeps using Retry-After or exponential backoff
- Collects minimal telemetry about attempts and total sleep time
- Appends items from json.items and follows json.links.next

## References
- RESTMessageV2 API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/concept/c_RESTMessageV2API.html
- Direct RESTMessageV2 example
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/RESTMessageV2/reference/r_DirectRESTMessageV2Example.html
- Inbound rate limiting and Retry-After header
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/inbound-REST-API-rate-limiting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Script Include: RestGetWithBackoff
* Purpose: Safely perform RESTMessageV2 GET requests with retry handling,
* exponential backoff, and simple pagination support.
*
* Example usage (Background Script):
* var helper = new RestGetWithBackoff();
* var data = helper.getAll({
* endpoint: 'https://api.example.com/v1/items',
* headers: { 'Authorization': 'Bearer ${token}' },
* maxRetries: 4,
* baseDelayMs: 750
* });
* gs.info('Fetched ' + data.length + ' records');
*/

var RestGetWithBackoff = Class.create();
RestGetWithBackoff.prototype = {
initialize: function() {},

/**
* Main entry point to fetch all pages of results.
* Handles retries, pagination, and aggregates results.
* @param {Object} options - endpoint, headers, maxRetries, baseDelayMs
* @returns {Array} all items combined from paginated responses
*/
getAll: function(options) {
var url = options.endpoint; // Initial API endpoint
var headers = options.headers || {}; // Optional request headers
var maxRetries = options.maxRetries || 5; // Maximum retry attempts per page
var baseDelayMs = options.baseDelayMs || 500;// Base delay for exponential backoff

var items = []; // Array to collect all items across pages
var attempts = 0; // Total number of REST calls
var totalSleepMs = 0; // Total delay time across retries

// Continue fetching until there are no more pages (links.next = null)
while (url) {
// Execute the REST call (with internal retry logic)
var res = this._execute('get', url, headers, maxRetries, baseDelayMs);
attempts += res.attempts; // Count total attempts made
totalSleepMs += res.sleptMs; // Sum total sleep time used in retries

// If non-success HTTP code, throw to stop execution
if (res.status < 200 || res.status >= 300)
throw 'HTTP ' + res.status + ' for ' + url + ': ' + res.body;

// Parse and validate JSON body
var json = this._safeJson(res.body);

// If body contains an 'items' array, append to results
if (Array.isArray(json.items)) items = items.concat(json.items);

// Get next page link if available (standard 'links.next' pattern)
url = json && json.links && json.links.next ? json.links.next : null;
}

// Log a completion summary
gs.info('REST helper complete. items=' + items.length +
', attempts=' + attempts +
', sleptMs=' + totalSleepMs);

return items;
},

/**
* Executes a REST call with retry and exponential backoff.
* Retries on HTTP 429 (Too Many Requests) or 5xx errors.
* @returns {Object} status, body, attempts, sleptMs
*/
_execute: function(method, url, headers, maxRetries, baseDelayMs) {
var attempt = 0;
var sleptMs = 0;

while (true) {
attempt++;

// Build the RESTMessageV2 object
var r = new sn_ws.RESTMessageV2();
r.setEndpoint(url);
r.setHttpMethod(method.toUpperCase());

// Apply custom headers (for example, auth tokens or content type)
Object.keys(headers).forEach(function(k) { r.setRequestHeader(k, headers[k]); });

// Execute the request
var resp = r.execute();
var status = resp.getStatusCode();
var body = resp.getBody();

// Success range (2xx)
if (status >= 200 && status < 300) {
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
}

// Handle 429 (rate limit) or transient 5xx server errors
if (status === 429 || status >= 500) {
// Stop retrying if max reached
if (attempt >= maxRetries) {
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
}

// Honour Retry-After header if present; otherwise exponential delay
var retryAfter = Number(resp.getHeader('Retry-After')) || 0;
var delayMs = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * baseDelayMs;

// Log retry details for visibility in system logs
gs.info('Retrying ' + url + ' after ' + delayMs +
' ms due to HTTP ' + status + ' (attempt ' + attempt + ')');

gs.sleep(delayMs); // Wait before retrying
sleptMs += delayMs;
continue;
}

// Non-retryable failure (e.g., 4xx not including 429)
return { status: status, body: body, attempts: attempt, sleptMs: sleptMs };
}
},

/**
* Safe JSON parser that throws descriptive error on invalid JSON.
* @param {String} body - raw HTTP response text
* @returns {Object} parsed JSON
*/
_safeJson: function(body) {
try {
return JSON.parse(body || '{}');
} catch (e) {
throw 'Invalid JSON: ' + e.message;
}
},

type: 'RestGetWithBackoff'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Background Script usage example for RestGetWithBackoff
(function() {
var helper = new RestGetWithBackoff();
var data = helper.getAll({
endpoint: 'https://api.example.com/v1/things?limit=100',
headers: { 'Authorization': 'Bearer ${token}' },
maxRetries: 4,
baseDelayMs: 750
});
gs.info('Fetched ' + data.length + ' items total');
})();
Loading