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