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