I am getting frustrated with this error :
main.js:86 GET https://api.spotify.com/v1/me/top/artists net::ERR_ABORTED 403 (Forbidden)
.
My JS code looks like this:
function getTopArtists(accessToken) {
return fetch("https://api.spotify.com/v1/me/top/artists", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}
async function displayTopArtistsInConsole(profile) {
const accessToken = ACCESS_TOKEN;
getTopArtists(accessToken)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Failed to fetch top artists");
}
})
.then((data) => {
console.log("Top Artists:", data.items);
})
.catch((error) => {
console.error(error);
});
}
I have tried debugging my code but I really cannot find any solution to my problem, neither by reading all the documentation on Spotify API. I am sure that my token is not expired at all because it can last up to 1 hour and I am running the code at the exact moment I open my HTML page. Could it have to do with the async
keyword or something? I can provide the full code here thanks in advance :
const clientId = "50a6862feb5a4139af099a0ca99e267a";
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
let ACCESS_TOKEN = null;
if (!code) {
document.getElementById("sign-in").addEventListener("click", () => {
redirectToAuthCodeFlow(clientId);
});
} else {
const accessToken = await getAccessToken(clientId, code).then();
// fetch("https://api.spotify.com/v1/me/top/tracks").then((res) => console.log(res));
const profile = await fetchProfile(accessToken);
populateUI(profile);
}
function generateCodeVerifier(length) {
let text = "";
let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
async function generateCodeChallenge(codeVerifier) {
const data = new TextEncoder().encode(codeVerifier);
const digest = await window.crypto.subtle.digest("SHA-256", data);
return btoa(String.fromCharCode.apply(null, [...new Uint8Array(digest)]))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
export async function redirectToAuthCodeFlow(clientId) {
const verifier = generateCodeVerifier(128);
const challenge = await generateCodeChallenge(verifier);
localStorage.setItem("verifier", verifier);
const params = new URLSearchParams();
params.append("client_id", clientId);
params.append("response_type", "code");
params.append("redirect_uri", "http://localhost:5173/callback");
params.append("scope", "user-read-private user-read-email");
params.append("code_challenge_method", "S256");
params.append("code_challenge", challenge);
document.location = `https://accounts.spotify.com/authorize?${params.toString()}`;
}
export async function getAccessToken(clientId, code) {
const verifier = localStorage.getItem("verifier");
const params = new URLSearchParams();
params.append("client_id", clientId);
params.append("grant_type", "authorization_code");
params.append("code", code);
params.append("redirect_uri", "http://localhost:5173/callback");
params.append("code_verifier", verifier);
const result = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params,
});
const { access_token } = await result.json();
ACCESS_TOKEN = access_token;
return access_token;
}
async function fetchProfile(token) {
const result = await fetch("https://api.spotify.com/v1/me", {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
return await result.json();
}
function getTopArtists(accessToken) {
return fetch("https://api.spotify.com/v1/me/top/artists", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}
async function displayTopArtistsInConsole(profile) {
const accessToken = ACCESS_TOKEN;
getTopArtists(accessToken)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Failed to fetch top artists");
}
})
.then((data) => {
console.log("Top Artists:", data.items);
})
.catch((error) => {
console.error(error);
});
}
function populateUI(profile) {
console.log(profile);
const profileSection = document.createElement("section");
profileSection.setAttribute("id", "profile");
const h2 = document.createElement("h2");
h2.innerHTML = 'Il tuo profilo <span id="displayName"></span>';
profileSection.appendChild(h2);
const avatarSpan = document.createElement("span");
avatarSpan.setAttribute("id", "avatar");
profileSection.appendChild(avatarSpan);
const ul = document.createElement("ul");
ul.id = "lista";
const profileDataElements = [
{ label: "User ID", id: "id" },
{ label: "Email", id: "email" },
{ label: "Spotify URI", id: "uri", isLink: true },
{ label: "Link", id: "url", isLink: true },
{ label: "Profile Image", id: "imgUrl" },
];
profileDataElements.forEach((data) => {
const li = document.createElement("li");
const span = document.createElement("span");
span.setAttribute("id", data.id);
li.textContent = `${data.label}: `;
if (data.isLink) {
const link = document.createElement("a");
link.setAttribute("id", data.id);
link.setAttribute("href", "#");
li.appendChild(link);
} else {
li.appendChild(span);
}
ul.appendChild(li);
});
profileSection.appendChild(ul);
document.body.appendChild(profileSection);
document.getElementById("displayName").innerText = profile.display_name;
if (profile.images[0]) {
const profileImage = new Image(200, 200);
profileImage.src = profile.images[0].url;
document.getElementById("avatar").appendChild(profileImage);
document.getElementById("imgUrl").innerText = profile.images[0].url;
}
document.getElementById("id").innerText = profile.id;
document.getElementById("email").innerText = profile.email;
document.getElementById("uri").innerText = profile.uri;
document.getElementById("uri").setAttribute("href", profile.external_urls.spotify);
document.getElementById("url").innerText = profile.href;
document.getElementById("url").setAttribute("href", profile.href);
document.getElementById("profile").style.display = "block";
document.getElementById("nota").remove();
document.getElementById("nota").remove();
document.getElementById("sign-in").remove();
displayTopArtistsInConsole(profile);
}
document.addEventListener("DOMContentLoaded", async function () {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
if (code) {
const accessToken = await getAccessToken(clientId, code);
const profile = await fetchProfile(accessToken);
populateUI(profile);
}
});
You've asked for the token issued by the authentication service to be authorized for the scopes user-read-private
and user-read-email
.
If you review the documentation for /me/top/{type}
, you'll likely notice that the scope user-top-read
is required to retrieve this information.
You'll need to request this scope as part of your call to /authorize
in your method redirectToAuthCodeFlow
:
params.append("scope", "user-read-private user-read-email user-top-read");