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.
JavaScript
x
12
12
1
Exception: Attribute is specified with no value: url
2
at fetch(Library:38:41)
3
at onRetryAfter(Library:84:20)
4
at onError(Library:109:24)
5
at readResponse(Library:78:16)
6
at fetch(Library:38:16)
7
at onRetryAfter(Library:84:20)
8
at onError(Library:109:24)
9
at readResponse(Library:78:16)
10
at [unknown function](Library:63:24)
11
at fetchAll(Library:62:43)
12
Log
JavaScript
1
32
32
1
// error
2
URL: https://api.spotify.com/v1/search/?q=x%20ambassadors%20unsteady&type=track&limit=1
3
Code: 429
4
Params: { headers: { Authorization: 'Bearer ***' },
5
payload: undefined,
6
muteHttpExceptions: true }
7
Content: {
8
"error": {
9
"status": 429,
10
"message": "API rate limit exceeded"
11
}
12
}
13
14
// info with url
15
https://api.spotify.com/v1/search/?q=x%20ambassadors%20unsteady&type=track&limit=1 { headers: { Authorization: 'Bearer ***' },
16
payload: undefined,
17
muteHttpExceptions: true }
18
19
// Error
20
URL: undefined
21
Code: 429
22
Params: {}
23
Content: {
24
"error": {
25
"status": 429,
26
"message": "API rate limit exceeded"
27
}
28
}
29
30
// Info without url
31
undefined { muteHttpExceptions: true }
32
Calling
Call fetchAll
into SpotifyRequest.getAll
JavaScript
1
51
51
1
function multisearchTracks(textArray) {
2
return SpotifyRequest.getAll(
3
textArray.map((text) =>
4
Utilities.formatString(
5
'%s/search/?%s',
6
API_BASE_URL,
7
CustomUrlFetchApp.parseQuery({
8
q: text,
9
type: 'track',
10
limit: '1',
11
})
12
)
13
)
14
).map((response) => response && response.items ? response.items[0] : {});
15
}
16
17
const SpotifyRequest = (function () {
18
return {
19
getAll: getAll,
20
};
21
22
23
function getAll(urls) {
24
let requests = [];
25
urls.forEach((url) =>
26
requests.push({
27
url: url,
28
headers: getHeaders(),
29
method: 'get',
30
})
31
);
32
return CustomUrlFetchApp.fetchAll(requests).map((response) => extractContent(response));
33
}
34
35
function extractContent(response) {
36
if (!response) return;
37
let keys = Object.keys(response);
38
if (keys.length == 1 && !response.items) {
39
response = response[keys[0]];
40
}
41
return response;
42
}
43
44
function getHeaders() {
45
return {
46
Authorization: 'Bearer ' + Auth.getAccessToken(),
47
};
48
}
49
})();
50
51
Full code
JavaScript
1
107
107
1
const CustomUrlFetchApp = (function () {
2
let countRequest = 0;
3
return {
4
fetch: fetch,
5
fetchAll: fetchAll,
6
parseQuery: parseQuery,
7
getCountRequest: () => countRequest,
8
};
9
10
function fetch(url, params = {}) {
11
countRequest++;
12
params.muteHttpExceptions = true;
13
return readResponse(UrlFetchApp.fetch(url, params));
14
}
15
16
function fetchAll(requests) {
17
countRequest += requests.length;
18
requests.forEach((request) => (request.muteHttpExceptions = true));
19
let responseArray = [];
20
let limit = 30;
21
let count = Math.ceil(requests.length / limit);
22
for (let i = 0; i < count; i++) {
23
const requestPack = requests.splice(0, limit);
24
const responseRaw = UrlFetchApp.fetchAll(requestPack);
25
const responses = responseRaw.map((response, index) => {
26
return readResponse(response, requestPack[index].url, {
27
headers: requestPack[index].headers,
28
payload: requestPack[index].payload,
29
muteHttpExceptions: requestPack[index].muteHttpExceptions,
30
});
31
});
32
Combiner.push(responseArray, responses);
33
}
34
return responseArray;
35
}
36
37
function readResponse(response, url, params = {}) {
38
if (isSuccess(response.getResponseCode())) {
39
return onSuccess();
40
}
41
return onError();
42
43
function onRetryAfter() {
44
let value = response.getHeaders()['Retry-After'] || 2;
45
console.error('Ошибка 429. Пауза', value);
46
Utilities.sleep(value > 60 ? value : value * 1000);
47
return fetch(url, params);
48
}
49
50
function tryFetchOnce() {
51
Utilities.sleep(3000);
52
countRequest++;
53
response = UrlFetchApp.fetch(url, params);
54
if (isSuccess(response.getResponseCode())) {
55
return onSuccess();
56
}
57
writeErrorLog();
58
}
59
60
function onSuccess() {
61
let type = response.getHeaders()['Content-Type'] || '';
62
if (type.includes('json')) {
63
return parseJSON(response);
64
}
65
return response;
66
}
67
68
function onError() {
69
writeErrorLog();
70
let responseCode = response.getResponseCode();
71
if (responseCode == 429) {
72
return onRetryAfter();
73
} else if (responseCode >= 500) {
74
return tryFetchOnce();
75
}
76
}
77
78
function isSuccess(code) {
79
return code >= 200 && code < 300;
80
}
81
82
function writeErrorLog() {
83
console.error('URL:', url, 'nCode:', response.getResponseCode(), 'nParams:', params, 'nContent:', response.getContentText());
84
}
85
}
86
87
function parseJSON(response) {
88
let content = response.getContentText();
89
return content.length > 0 ? tryParseJSON(content) : { msg: 'Пустое тело ответа', status: response.getResponseCode() };
90
}
91
92
function tryParseJSON(content) {
93
try {
94
return JSON.parse(content);
95
} catch (e) {
96
console.error(e, e.stack, content);
97
return [];
98
}
99
}
100
101
function parseQuery(obj) {
102
return Object.keys(obj)
103
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
104
.join('&');
105
}
106
})();
107
Advertisement
Answer
Issue:
JavaScript
1
10
10
1
function readResponse(response, url, params = {}) {
2
/*stuff*/
3
}
4
5
/*...*/
6
function fetch(url, params = {}) {
7
/*...*/
8
return readResponse(UrlFetchApp.fetch(url, params));
9
}
10
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:
JavaScript
1
2
1
return readResponse(UrlFetchApp.fetch(url, params), url, params);
2