spotify / web-api

This issue tracker is no longer used. Join us in the Spotify for Developers forum for support with the Spotify Web API ➡️ https://community.spotify.com/t5/Spotify-for-Developers/bd-p/Spotify_Developer
983 stars 79 forks source link

request Spotify's /api/token got errors: 400 Bad Request #321

Closed fluency03 closed 7 years ago

fluency03 commented 8 years ago

According to the Client Credentials Flow part of Spotiy's web api authorization guide, I made the following request:

POST https://accounts.spotify.com/api/token
grant_type=client_credentials
Authorization: Basic btoa(CLIENT_ID + ':' + CLIENT_SECRET)

and

Content-Type: application/json

But get the error:

415 Unsupported Media Type

{
    "error": "server_error",
    "error_description": "Unexpected status: 415"
}

Based on this and this SO questions, I changed the

Content-Type: application/x-www-form-urlencoded

However, I got 400 Bad Request.

Anyone has other possible solutions?

jsimonsson commented 8 years ago

Seeing how you probably were posting JSON (since you set the content type to that) then that is probably the problem, you are supposed to pass parameters as normal when the content type is application/x-www-form-urlencoded.

fluency03 commented 8 years ago

@jsimonsson I have changed to application/x-www-form-urlencoded but I got 400 Bad Request

jsimonsson commented 8 years ago

But what are you posting? If you are posting a JSON then that is wrong.

fluency03 commented 8 years ago

@jsimonsson I just would like to do the Client Credentials Flow process. I am not posting any other things.

jsimonsson commented 8 years ago

omg, I just miss read authorization code flow with client credentials :) Never mind me! Let me check your issue again.

jsimonsson commented 8 years ago

I'm gonna go ahead and guess that the Authorization header is the culprit. Do you get an error message aswell besides a 400 Bad Request?

fluency03 commented 8 years ago

@jsimonsson I am doing Client Credentials Flow not Authorization Code Flow. I am currently just trying this using this: https://apigee.com/console/

jsimonsson commented 8 years ago

Well, does the console understand btoa(CLIENT_ID + ':' + CLIENT_SECRET) or have you done the base64 part before and copying it into the header fields section in that console?

fluency03 commented 8 years ago

@jsimonsson I am actually planning to call Spotify's api using the client: WeChat WeApp, as following:

function getAccessToken() {
  wx.request({
    url: 'https://accounts.spotify.com/api/token',
    method: 'POST',
    data: {
      grant_type: 'client_credentials'
    },
    header: {
      'Authorization': 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET),
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    success: function(res) {
      console.log('getAccessToken-S');
      console.log(res.data);
      that.setData({
        token: res.data
      })
    },
    complete: function() {
      console.log('getAccessToken-C');
    }
  });
}
fluency03 commented 8 years ago

@jsimonsson when using apigee concole, I directly use the base64 version of CLIENT_ID + ':' + CLIENT_SECRET, which I generated from https://www.base64encode.org/

jsimonsson commented 8 years ago

Ok, then I have no idea, I just tried with curl and it works fine for me. Is this code above running in a browser or not? If it is running in a browser then I would advice against having the secret and client ID there, if it is running in for example node.js then I have no idea. You should get an error message when you get 400 BAD REQUEST tho, so if you do, please post it here.

fluency03 commented 8 years ago

The code is running as javascript in WeChat WeApp's IDE.


The following is what I got from apigee console:

HTTP/1.1 400 Bad Request
Date: Tue, 04 Oct 2016 14:00:03 GMT
Content-Length: 0
Set-Cookie: csrf_token=AQC6XN2Qeutx7xik3bpyMdg7C_r7gQy8VHVczz4GccQZIJGpLNlWTyECmKmG0VLYUCfn7kw_jXBm7OROGg;Version=1;Domain=accounts.spotify.com;Path=/;Secure
Connection: keep-alive
Server: nginx
asmitter commented 8 years ago

@fluency03 It's a little tricky to advise without seeing the raw requests you are making. When you get a 400 Bad Request error, you should also get a body containing a message about the problem and it may contain the exact reason for failure.

My advice would be to try the flow without a framework. I've created a gist here that you can try out: https://gist.github.com/asmitter/796e2906a4f2b4fe9b660f8562383412

If that succeeds, then there is likely something up with how the framework is making requests.

hughrawlinson commented 7 years ago

Hey @fluency03 - did @asmitter's gist work for you? If not, please feel free to reopen this issue and we'll see if we can help.

tetreault commented 7 years ago

I'm currently having this issue. I tried both the Web API and this made by what looks like 2 spotify devs. i've tried locally and on an EC2 instance. I have no luck in either scenario -- same issue "Bad Request". I feel like there HAS to be something more here. I can pull random artist album's just fine, but the issue seems to be your authentication flow. It doesn't seem like an isolated incident either.

IMO posting a gist of the cURL example doesn't help much either when the cURL examples in the official Spotify Web API docs yield same bad results.

tetreault commented 7 years ago

I reference it in comments in threads here and here.

Any help is for sure appreciated. I need this API call to work so I can retrieve a simple playlist. I already have the node API working if i pull a random artist's album, but the issue here is authorization.

tetreault commented 7 years ago

Even tried different versions of node and npm just for the hell of it since nothing else is working to make progress on figuring out why this step's failing :(

tetreault commented 7 years ago

@hughrawlinson if you dont mind, could you reopen when you get a chance?

hughrawlinson commented 7 years ago

@tetreault happy to! Could you please give us some more context as to what you're trying that's not working? It might be that this isn't the same as the original issue.

I've just tried the client credentials flow myself using curl and it seems to be working fine, so this might be an issue with the spotify-web-api-node wrapper.

tetreault commented 7 years ago

@hughrawlinson thanks for reaching back out!

I'm not using the WeChat WeApp - I'm just trying to successfully do the Client Credentials flow so i can grab a playlist i've made under a dev account I'm treating like a bot.

I've followed along the docs here and even the cURL didn't work for me.

With the Node API I was trying the spotifyApi.clientCredentialsGrant() method of getting a token but I receive the same "Bad Request".

I've never received a 415 Media Type error, just the 400 Bad Request.

Here's the code - secret is hidden in a .env file, no worries.

'use strict';

import express from 'express';
import path from 'path';
import fs from 'fs';
import http from 'http';
import https from 'https';
import Clarifai from 'clarifai';
import request from 'request'; 
import SpotifyWebApi from 'spotify-web-api-node';
import config from './config';
import bodyParser from 'body-parser';
import CircularJSON from 'circular-json';

const app = express();

app.use(express.static(path.join(__dirname, '../public')));
app.use(bodyParser.json({limit: '6mb'}));
app.set('view engine', 'ejs');

// Router for navigating us to the site. 
app.get('/', (req, res) => {
  res.render('pages/index.ejs');
});

// Router for generating the Spotify playlist
app.get('/spotify', (req, res) => {
  console.log('test'); 

  const spotifyValues = {
    user: config.spotify.user,
    clientId: config.spotify.id,
    clientSecret: config.spotify.secret,
    redirectUri: 'http://XXXXXXX.compute.amazonaws.com/'
  };

// This is the Web API attempt that didn't work
/*
  // your application requests authorization
  var authOptions = {
    url: 'https://accounts.spotify.com/api/token',
    headers: {
      'Authorization': 'Basic ' + (new Buffer(spotifyValues.clientId + ':' + spotifyValues.clientSecret).toString('base64'))
    },
    form: {
      grant_type: 'client_credentials'
    },
    json: true
  };

  console.log(authOptions);

  request.post(authOptions, function(error, response, body) {
      if (!error && response.statusCode === 200) {

      // use the access token to access the Spotify Web API
      var token = body.access_token;
      var options = {
        url: 'https://api.spotify.com/v1/users/couplesmixtape',
        headers: {
          'Authorization': 'Bearer ' + token
        },
        json: true
      };

      console.log(token, options); 

      request.get(options, function(error, response, body) {
        console.log(body);
      });
    }
  });
*/

  let spotifyApi = new SpotifyWebApi({
    clientId : config.spotify.id,
    clientSecret : config.spotify.secret,
    redirectUri: 'http://XXXXXXXX.compute.amazonaws.com/'
  });

  // Retrieve an access token
  spotifyApi.clientCredentialsGrant()
    .then(function(data) {
      console.log('The access token expires in ' + data.body['expires_in']);
      console.log('The access token is ' + data.body['access_token']);

      // Save the access token so that it's used in future calls
      spotifyApi.setAccessToken(data.body['access_token']);
    }, function(err) {
      console.log('Something went wrong when retrieving an access token', err.message);
    });

  if (config.debug) console.log('A call was made to the spotify endpoint');

});

app.listen(config.ports.http);

if (config.debug) console.log(`App is running on ${config.ports.http}.`);
tetreault commented 7 years ago

@hughrawlinson The gist from @asmitter worked on my end once I removed the | jq . part. This means it has to be something malforming my request in a way that Spotify doesn't like. I need to use bodyParser.json (this line here: app.use(bodyParser.json({limit: '6mb'}));) for a step that transmits big base64 representations to the server via a POST.

What I'm going to try at this rate is doing the spotify step i need inside a lambda script exposed by API Gateway. Not ideal, but if it works in that microservice then its good enough for right now.

tetreault commented 7 years ago

@hughrawlinson I tried putting the client auth in a lambda script to see what happens. It doesn't work there either, even though the shell script example posted by @asmitter did for the same client ID and Secret.

context.succeed spits out null. Really at a loss.

'use strict';

var request = require('request');

exports.handler = function (event, context) {
  console.log(event);
  var clientId = 'YYYYYYYYYYYYYYYYYYY';
  var clientSecret = 'XXXXXXXXXXXXXXXXXXX';
  var authOptions = {
    url: 'https://accounts.spotify.com/api/token',
    headers: {
      'Authorization': 'Basic ' + (new Buffer(clientId + ':' + clientSecret).toString('base64'))
    },
    form: {
      grant_type: 'client_credentials'
    },
    json: true
  };

  request.post(authOptions, function(error, response, body) {
    if (!error && response.statusCode === 200) {

      // use the access token to access the Spotify Web API
      var token = body.access_token;
      var options = {
        url: 'https://api.spotify.com/v1/users/couplesmixtape',
        headers: {
          'Authorization': 'Bearer ' + token
        },
        json: true
      };

      request.get(options, function(error, response, body) {
        context.succeed(body);
      });
    }
  });
};
tetreault commented 7 years ago

@hughrawlinson I ended up getting it working with the node wrapper for the web api. The source of the problem seems extremely random though... So I know i've been all over the place but I finally got the access token being generated properly. If possible - i'd like your feedback on this.

I store my API keys in a .env file. I have a config.js file that pulls in the values from the .env file via process.env.FIELD_NAME. Therein lies the issue. If i hard code my API keys when we instantiate a new SpotifyWebApi it is functional but otherwise all attempts fail.

This is why I suppose it took so many methods till I found this because I've never had any issue reading in values from a .env file before using the dotenv module.

I even scanned the .env file for any invisible characters that somehow got introduced thru VIM that were messing things up but there was nothing there.

Furthermore I have Clarifai API keys stored in the .env file and there was zero issue getting those API keys from that file into the instantiation of a new Clarifai App. The issue here was only present with the spotify keys.

I know the keys are correct too because I copied them from the .env file and pasted them into my server.js file - I didn't re-copy them from the Spotify Dev dashboard.

To make matters even more confusing I printed out the keys from .env -> config.js -> server.js.

For the spotify Client ID only there is an extra single quote being added to the end of the spotify Client ID. However, the Client Secret is fine, totally unscathed. I checked all through - no extra single quote anywhere plus again the other API keys in the .env file dont have this issue...

Very curious, I wish I knew why this was happening.

hughrawlinson commented 7 years ago

@tetreault is it possible that the Spotify credentials were at the end of your .env file, and weren't followed by a newline? Depending on how you're reading it, this can cause issues sometimes. I'll close this issue as it looks like it's solved, but I'd like to hear back on whether or not this could've been the issue. Hope your Spotify integration is smooth sailing from here on out 😄

tetreault commented 7 years ago

@hughrawlinson nope that is the weird part the spotify credentials were in the middle of the .env file. I had API secret at the bottom for the Clarifai API and its ID & Secret weren't corrupted at all. But yeah, this definitely seems like a dotenv module problem. Thanks for being patient!

sami4064 commented 7 years ago

Any solution for this 400 BAD Request? I am calling web api from ios app.

tetreault commented 7 years ago

@sami4064 double check your api keys to make sure theyre what youre expecting them to be. In my case, I was using the env module to store my keys but the regex for it, for some wild reason, was choking on my spotify key thus it was resulting in a bad request.

sdxism commented 7 years ago

If anyone else is having this issue, ensure you are setting the Content-Type to application/x-www-form-urlencoded

sample just using https

`var auth = 'Basic ' + new Buffer(clientID + ':' + clientSecret).toString('base64'); var data = qs.stringify({'grant_type':'client_credentials'});

var options = {
    hostname:'accounts.spotify.com',
    path:'/api/token',
    method: 'POST',
    headers:{
        'Authorization': auth,
        'Accept':'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
    }
};

var r = https.request(options, function(res){
    console.log(res.statusCode);
    res.on('data', function(c){
        console.log(JSON.parse(c));
    });
});

r.write("grant_type=client_credentials");
r.end();`
jsejcksn commented 7 years ago

I'm seeing this issue also while attempting the Client Credentials flow—I have been unsuccessful in acquiring my auth token and am getting no body response from the 400 error. I get a successful response using the curl method, but not using JavaScript.

Can anyone help me troubleshoot? I have included all of my code (except that I've redacted my actual client keys in the first constant declaration) and the entire console output, which was running locally in Chrome 60.0.3112.90 (so I'm not exposing my client keys). I have also linked to the same content in a gist so you can match the console line numbers to the code: https://gist.github.com/jsejcksn/8ad111106d9001b60d5bf631df5da986

console.log:

spotify-api-test.js:23 POST https://accounts.spotify.com/api/token 400 (Bad Request)
(anonymous) @ spotify-api-test.js:23
spotify-api-test.js:30 Fetch problem: Error: Network response was not ok.

spotify-api-test.js:

const myClientInfo = {
  clientID: '<client_id>',
  clientSecret: '<client_secret>'
};

const myAuthHeaders = {
  Authorization: `Basic ${btoa(`${myClientInfo.clientID}:${myClientInfo.clientSecret}`)}`, // Won't work for unicode characters, but I think the Client ID is always ASCII
  'Content-Type': 'application/x-www-form-urlencoded'
};

const myAuthBody = new FormData();
myAuthBody.append('grant_type', 'client_credentials');

const myAuthInit = {
  method: 'POST',
  headers: myAuthHeaders,
  body: myAuthBody,
  mode: 'no-cors'
};

const myAuthRequest = new Request('https://accounts.spotify.com/api/token', myAuthInit);

fetch(myAuthRequest).then(res => {
  if (res.ok) {
    return res.json();
  }
  throw new Error('Network response was not ok.');
})
.then(data => console.log(data))
.catch(err => console.log(`Fetch problem: ${err}`));
Agnostic commented 7 years ago

@jsejcksn Try this, I am using the axios package but this code is working for me:

     axios({
        url: 'https://accounts.spotify.com/api/token',
        method: 'post',
        params: {
          grant_type: 'client_credentials'
        },
        headers: {
          'Accept':'application/json',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        auth: {
          username: 'YOUR-CLIENT-ID',
          password: 'YOUR-CLIENT-SECRET'
        }
      }).then(function(response) {
          console.log(response);
      }).catch(function(error) {
      });
Agnostic commented 7 years ago

The issue is that grant_type must be in the query params, not as a form/json data.

jsejcksn commented 7 years ago

@Agnostic Thanks for taking a look. I tried various methods of supplying the parameters (directly modifying the url, supplying as body content, etc.) in the query component of the url https://accounts.spotify.com/api/token?grant_type=client_credentials, but the result didn't change—still 400 (Bad Request).

Any other ideas?

Agnostic commented 7 years ago

Try to use my code with axios, if it does not work, then you have a wrong value somewhere https://github.com/mzabriskie/axios

jsejcksn commented 7 years ago

I modified my script to now use a FormData object:

spotify-api-test.js

const myClientInfo = { // but using actual client ID and secret values
  clientID: '<client_id>',
  clientSecret: '<client_secret>'
};

const myAuthUrl = new URL('https://accounts.spotify.com/api/token');

const myAuthHeaders = {
  'Content-Type': 'application/x-www-form-urlencoded',
  Authorization: `Basic ${btoa(`${myClientInfo.clientID}:${myClientInfo.clientSecret}`)}` // Won't work for unicode characters, but I think the Client ID is always ASCII
};

const myAuthQueryParams = {
  grant_type: 'client_credentials'
};

let myAuthFormBody = new FormData();
appendObjToFormBody(myAuthQueryParams, myAuthFormBody);

const myAuthInit = {
  method: 'POST',
  headers: myAuthHeaders,
  body: myAuthFormBody,
  mode: 'no-cors'
};

const myAuthRequest = new Request(myAuthUrl, myAuthInit);

fetch(myAuthRequest)
.then(res => {
  if (res.ok) {
    return res.json();
  }
  throw new Error('Network response was not ok.');
})
.then(data => console.log(data))
.catch(err => console.log(`Fetch problem: ${err}`));

function appendObjToFormBody (obj, form) {
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      form.append(prop, obj[prop]);
    }
  }
}

but I still get the error 400 (Bad Request) message in console. However, I inspected the token in Chrome > Dev Tools > Network > Name > Token > Preview and discovered error: "invalid_client", which was not present in the error message in the console. It makes me wonder if Spotify has specifically blocked browser requests for the Client Credentials flow.

@Agnostic I could try with axios, but I don't want to add that lib to my code, so it wouldn't help solve the problem at hand.

Agnostic commented 7 years ago

Hi @jsejcksn, the issue is that the grant_type param should be in the url, not as a form.

1.- You need to remove this header: 'Content-Type': 'application/x-www-form-urlencoded',

2.- Remove body from myAuthInit: body: myAuthFormBody,

3.- Change myAuthUrl to: const myAuthUrl = new URL('https://accounts.spotify.com/api/token?grant_type=client_credentials');

I'm not sure if you need to remove mode: 'no-cors', but try and tell me if that works :)

jsejcksn commented 7 years ago

@Agnostic I appreciate your effort to help me sort this out—I am very confused about why I can't get this working and I can't seem to find even one vanilla JS browser example of this flow.

I tried what you said. When I commented a couple of days ago, I meant that I tried every possible method of supplying the parameters, including your recommendation. When I try as you recommend, the error is 400 (Bad Request) with these additional details in Dev Tools: error: "server_error", error_description: "Unexpected status: 400".

Agnostic commented 7 years ago

@jsejcksn A last chance...

Try again with this code:

const myClientInfo = { // but using actual client ID and secret values
  clientID: '<client_id>',
  clientSecret: '<client_secret>'
};

const myAuthUrl = new URL('https://accounts.spotify.com/api/token?grant_type=client_credentials');

const auth = btoa(`${myClientInfo.clientID}:${myClientInfo.clientSecret}`);

const myAuthHeaders = {
  Accept: 'application/json',
  Authorization: `Basic ${auth}`
};

const myAuthInit = {
  method: 'POST',
  headers: myAuthHeaders
};

const myAuthRequest = new Request(myAuthUrl, myAuthInit);

fetch(myAuthRequest)
.then(res => {
  if (res.ok) {
    return res.json();
  }
  throw new Error('Network response was not ok.');
})
.then(data => console.log(data))
.catch(err => console.log(`Fetch problem: ${err}`));
jsejcksn commented 7 years ago

@Agnostic Console log still errors 400 (Bad Request). Thanks for the effort. I'm just going to raise a new issue.

chacabuk commented 6 years ago

the answer it's simple: write the body on this way: grant_type=client_credentials

Not used {} or ""

happy-machine commented 6 years ago

Pretty sure the problem is that you cant post using the client credentials flow. I had this problem when i first started using the API

The method makes it possible to authenticate your requests to the Spotify Web API and to obtain a >higher rate limit than you would get without authentication. Note, however that this flow does not >include authorization and therefore cannot be used to access or manage a user’s private data. This >flow is described in RFC-6749.

You can use the authorization code flow server side in node

celoranta commented 5 years ago

Old thread, I know. However, I had the same issue and solved it, I believe:

I was setting the content type to 'application/x-www-form-urlencoded', but not actually encoding the string. Once I did both, it worked.

psychokiller666 commented 5 years ago

I have encountered the same problem with you, and I solved it. This method was used. Https://stackoverflow.com/questions/55460718/error-400-when-making-post-request-to-spotify-api-with-axios-on-express-js

import querystring from 'querystring';

$axios.post('https://accounts.spotify.com/api/token',querystring.stringify({ grant_type: 'authorization_code', code: route.query.code, redirect_uri: redirect_uri }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) }, }).then(res => { console.log(res) }).catch(error => { console.log(error) })

mcanvar commented 4 years ago

For more browser friendly approach I've came up to this at the end:


axios.post(  
'https://accounts.spotify.com/api/token',  \
    new URLSearchParams({  \
        grant_type: "client_credentials",  
        code:   'AQBxWnU0SB...l_jU',  
        redirect_uri: 'https://example.com/callback'  
    }).toString(),
    {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Authorization': 'Basic ' + document.getElementById('base64').value
        },
    }
).then(function (response) {
    console.log(response.data);
});
yelsayed commented 4 years ago

Beating a dead horse here but here is the pure VanillaJS solution with promises:

function ajax(request, url, headers, data) {
    var promise = new Promise((resolve, reject) => {
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.withCredentials = true;
        xmlHttp.onreadystatechange = function() {
            if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                resolve(xmlHttp.responseText);
            } else {
                reject(xmlHttp.responseText);
            }
        }
        xmlHttp.open(request, url);
        for (var key in headers) {
            if (headers.hasOwnProperty(key)) {
                xmlHttp.setRequestHeader(key, headers[key]);
            }
        }
        xmlHttp.send(data);
    });
    return promise;
}

function spotifyAuth() {
    var headers = { 
        "Authorization": "Basic " + CLIENT_SECRET_ID_ENCODED, 
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded"
    }

    var body = new URLSearchParams({  
        "code": code,
        "redirect_uri": redirectUri,
        "grant_type": "authorization_code"
    }).toString();

    ajax("POST", "https://accounts.spotify.com/api/token", headers, body)
    .then(function(resp) {
        console.log(resp);
    })
}