Skip to content
Advertisement

Exception – Attribute is specified with no value: url

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);
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement