The second call fetch
throw exception because it loses value of url
. Why?
See stack trace. Twice error 429 and call onRetryAfter
for each. But exception only for the second. The first fetch
has url
, the second loses it.
Exception: Attribute is specified with no value: url at fetch(Library:38:41) at onRetryAfter(Library:84:20) at onError(Library:109:24) at readResponse(Library:78:16) at fetch(Library:38:16) at onRetryAfter(Library:84:20) at onError(Library:109:24) at readResponse(Library:78:16) at [unknown function](Library:63:24) at fetchAll(Library:62:43)
Log
// error URL: https://api.spotify.com/v1/search/?q=x%20ambassadors%20unsteady&type=track&limit=1 Code: 429 Params: { headers: { Authorization: 'Bearer ***' }, payload: undefined, muteHttpExceptions: true } Content: { "error": { "status": 429, "message": "API rate limit exceeded" } } // info with url https://api.spotify.com/v1/search/?q=x%20ambassadors%20unsteady&type=track&limit=1 { headers: { Authorization: 'Bearer ***' }, payload: undefined, muteHttpExceptions: true } // Error URL: undefined Code: 429 Params: {} Content: { "error": { "status": 429, "message": "API rate limit exceeded" } } // Info without url undefined { muteHttpExceptions: true }
Calling
Call fetchAll
into SpotifyRequest.getAll
function multisearchTracks(textArray) { return SpotifyRequest.getAll( textArray.map((text) => Utilities.formatString( '%s/search/?%s', API_BASE_URL, CustomUrlFetchApp.parseQuery({ q: text, type: 'track', limit: '1', }) ) ) ).map((response) => response && response.items ? response.items[0] : {}); } const SpotifyRequest = (function () { return { getAll: getAll, }; function getAll(urls) { let requests = []; urls.forEach((url) => requests.push({ url: url, headers: getHeaders(), method: 'get', }) ); return CustomUrlFetchApp.fetchAll(requests).map((response) => extractContent(response)); } function extractContent(response) { if (!response) return; let keys = Object.keys(response); if (keys.length == 1 && !response.items) { response = response[keys[0]]; } return response; } function getHeaders() { return { Authorization: 'Bearer ' + Auth.getAccessToken(), }; } })();
Full code
const CustomUrlFetchApp = (function () { let countRequest = 0; return { fetch: fetch, fetchAll: fetchAll, parseQuery: parseQuery, getCountRequest: () => countRequest, }; function fetch(url, params = {}) { countRequest++; params.muteHttpExceptions = true; return readResponse(UrlFetchApp.fetch(url, params)); } function fetchAll(requests) { countRequest += requests.length; requests.forEach((request) => (request.muteHttpExceptions = true)); let responseArray = []; let limit = 30; let count = Math.ceil(requests.length / limit); for (let i = 0; i < count; i++) { const requestPack = requests.splice(0, limit); const responseRaw = UrlFetchApp.fetchAll(requestPack); const responses = responseRaw.map((response, index) => { return readResponse(response, requestPack[index].url, { headers: requestPack[index].headers, payload: requestPack[index].payload, muteHttpExceptions: requestPack[index].muteHttpExceptions, }); }); Combiner.push(responseArray, responses); } return responseArray; } function readResponse(response, url, params = {}) { if (isSuccess(response.getResponseCode())) { return onSuccess(); } return onError(); function onRetryAfter() { let value = response.getHeaders()['Retry-After'] || 2; console.error('Ошибка 429. Пауза', value); Utilities.sleep(value > 60 ? value : value * 1000); return fetch(url, params); } function tryFetchOnce() { Utilities.sleep(3000); countRequest++; response = UrlFetchApp.fetch(url, params); if (isSuccess(response.getResponseCode())) { return onSuccess(); } writeErrorLog(); } function onSuccess() { let type = response.getHeaders()['Content-Type'] || ''; if (type.includes('json')) { return parseJSON(response); } return response; } function onError() { writeErrorLog(); let responseCode = response.getResponseCode(); if (responseCode == 429) { return onRetryAfter(); } else if (responseCode >= 500) { return tryFetchOnce(); } } function isSuccess(code) { return code >= 200 && code < 300; } function writeErrorLog() { console.error('URL:', url, 'nCode:', response.getResponseCode(), 'nParams:', params, 'nContent:', response.getContentText()); } } function parseJSON(response) { let content = response.getContentText(); return content.length > 0 ? tryParseJSON(content) : { msg: 'Пустое тело ответа', status: response.getResponseCode() }; } function tryParseJSON(content) { try { return JSON.parse(content); } catch (e) { console.error(e, e.stack, content); return []; } } function parseQuery(obj) { return Object.keys(obj) .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`) .join('&'); } })();
Advertisement
Answer
Issue:
function readResponse(response, url, params = {}) { /*stuff*/ } /*...*/ function fetch(url, params = {}) { /*...*/ return readResponse(UrlFetchApp.fetch(url, params)); }
readResponse()
function accepts three arguments:response
, url
and params
, but fetch
only passes 1 argument: response
causing url
to be undefined
.
Solution:
Pass three arguments to the readResponse
Snippet:
return readResponse(UrlFetchApp.fetch(url, params), url, params);