400: Bad Request: When exchanging code for access token (Next.js)

As the title says, I got a 400 Bad Request error when I tried to get the code exchanged for an access token against https://app.asana.com/-/oauth_token.

I honestly can’t think of what could be causing this.
I will post the code and would appreciate anyone’s help.

const GetAsanaAccessToken = async (req, res) => {
  const body = {
    grant_type: 'authorization_code',
    client_id: process.env.NEXT_PUBLIC_ASANA_CLIENT_ID,
    client_secret: process.env.ASANA_CLIENT_SECRET,
    redirect_uri: process.env.NEXT_PUBLIC_ASANA_REDIRECT_URI,
    code: req.body.code // The code obtained in the previous flow goes here.
  console.log({ body });
  const url = 'https://app.asana.com/-/oauth_token';
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    body: JSON.stringify(body)
    .then((res) => {
      console.log({ res });
      return res.json();
    .catch((err) => {
      console.log({ err });
      return err;


export default GetAsanaAccessToken;

Then { res } will be like this

  res: Response {
    size: 0,
    timeout: 0,
    [Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
    [Symbol(Response internals)]: {
      url: 'https://app.asana.com/-/oauth_token',
      status: 400,
      statusText: 'Bad Request',
      headers: [Headers],
      counter: 0

Resolved. This is the cause.

body: JSON.stringify(body)

Replaced above with below, then worked.

body: Object.keys(body)
    .map((key) => `${key}=${encodeURIComponent(body[key])}`)

Because the original one resulted in


It worked when I made it look like the one below (I just made it by hand, so it really encodes slashes, etc.) instead of the one above.


I’ll keep saying it: there is a special place in heaven for people coming back to share the solution. Thank you :pray: :heart: