Skip to content

Commit 00e790f

Browse files
authored
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.
1 parent a1f5d27 commit 00e790f

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed
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+
};

0 commit comments

Comments
 (0)