TypeError: body used already for: https://auth.atlassian.com/oauth/token

1. Background

We are developing an application called WorkStats (https://workstats.dev/). WorkStats is a dashboard tool for engineers, project managers, and their teams to quantify their productivity, aggregating various numbers from GitHub, Asana, Slack, and Google Calendar.

And we are now thinking of adding API integration with Atlassian to display JIRA and Confluence data aggregation as well.

Dashboard - WorkStats - Google Chrome - 29 August 2022

2. Issue

I’ve done API integration with GitHub, Asana, Slack, Google Calendar, etc., so I thought I was familiar with it, HOWEVER, I got an error that I’ve never seen before and couldn’t solve, so I asked.

Here is the error.
TypeError: body used already for: https://auth.atlassian.com/oauth/token

I followed this flow below and the error occurred at the fourth step.

  1. [Good] Request code from WorkStats screen
  2. [Good] Log in to Atlassian and agree to the scope requested
  3. [Good] Redirected to WorkStats screen, code successfully retrieved in URL
  4. [Bad] Call API to exchange code for an access token

For some reason the API call was made twice, the first time was OK with status = 200, but the second time was Forbidden with status = 403. Both returned an empty object as a result.

3. Source and Logs

The API source is here;

import type { NextApiRequest, NextApiResponse } from 'next';

type ResponseData = {
  access_token: string;
  refresh_token: string;
  expires_in: number; // expiry time of access_token in second
  scope: string;
};

interface bodyType {
  grant_type: 'authorization_code' | 'refresh_token';
  client_id: string;
  client_secret: string;
  code: string;
  redirect_uri: string;
  // refresh_token: string; // Set if the access token has expired
  [key: string]: string; // To avoid type error ts(7053) in params[key]
}

// Exchange a code for an Atlassian access token
// See the details here; https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/#2--exchange-authorization-code-for-access-token
const GetAtlassianAccessToken = async (
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>
) => {
  const body: bodyType = {
    grant_type: req.body.grant_type,
    client_id: process.env.NEXT_PUBLIC_ATLASSIAN_CLIENT_ID || '',
    client_secret: process.env.ATLASSIAN_CLIENT_SECRET || '',
    code: req.body.code,
    redirect_uri:
      `${process.env.NEXT_PUBLIC_ORIGIN}/user-settings?atlassian=true` || ''
    // refresh_token: req.body.refresh_token || ''
  };
  console.log('body is: ', body);
  const url = 'https://auth.atlassian.com/oauth/token';

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  })
    .then((res) => {
      console.log('res is: ', res);
      console.log('res.json() is: ', res.json());
      // return res;
      return res.json();
    })
    .catch((err) => {
      console.log('err is: ', err);
      return err;
    });

  res.status(200).json(response);
};

export default GetAtlassianAccessToken;

And console.log for body is here;

body is:  {
  grant_type: 'authorization_code',
  client_id: 'aNxTpmK1Cie8......',
  client_secret: '1w7Gi19AsR5dqdJhD2..............',
  code: 'GPV-KKBYCxbmm...................',
  redirect_uri: 'http://localhost:3000/user-settings?atlassian=true'
}

And console.log for res is here;

res is:  Response {
  size: 0,
  timeout: 0,
  [Symbol(Body internals)]: {
    body: Gunzip {
      _writeState: [Uint32Array],
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      bytesWritten: 0,
      _handle: [Zlib],
      _outBuffer: <Buffer 7b 22 61 63 63 65 73 73 5f 74 6f 6b 65 6e 22 3a 22 65 79 4a 68 62 47 63 69 4f 69 4a 53 55 7a 49 31 4e 69 49 73 49 6e 52 35 63 43 49 36 49 6b 70 58 56 ... 16334 more bytes>,
      _outOffset: 0,
      _chunkSize: 16384,
      _defaultFlushFlag: 2,
      _finishFlushFlag: 2,
      _defaultFullFlushFlag: 3,
      _info: undefined,
      _maxOutputLength: 4294967295,
      _level: -1,
      _strategy: 0,
      [Symbol(kCapture)]: false,
      [Symbol(kTransformState)]: [Object],
      [Symbol(kError)]: null
    },
    disturbed: false,
    error: null
  },
  [Symbol(Response internals)]: {
    url: 'https://auth.atlassian.com/oauth/token',
    status: 200,
    statusText: 'OK',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    counter: 0
  }
}

But secondly console.log for res shows;

res is:  Response {
  size: 0,
  timeout: 0,
  [Symbol(Body internals)]: {
    body: Gunzip {
      _writeState: [Uint32Array],
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 5,
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: true,
      bytesWritten: 0,
      _handle: [Zlib],
      _outBuffer: <Buffer 7b 22 65 72 72 6f 72 22 3a 22 69 6e 76 61 6c 69 64 5f 67 72 61 6e 74 22 2c 22 65 72 72 6f 72 5f 64 65 73 63 72 69 70 74 69 6f 6e 22 3a 22 49 6e 76 61 ... 16334 more bytes>,
      _outOffset: 0,
      _chunkSize: 16384,
      _defaultFlushFlag: 2,
      _finishFlushFlag: 2,
      _defaultFullFlushFlag: 3,
      _info: undefined,
      _maxOutputLength: 4294967295,
      _level: -1,
      _strategy: 0,
      [Symbol(kCapture)]: false,
      [Symbol(kTransformState)]: [Object],
      [Symbol(kError)]: null
    },
    disturbed: false,
    error: null
  },
  [Symbol(Response internals)]: {
    url: 'https://auth.atlassian.com/oauth/token',
    status: 403,
    statusText: 'Forbidden',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    counter: 0
  }
}

Hi @HiroshiNishio. I’ve run into this issue many moons ago. Regarding the error “body used already”, that can occur when you try to pull from the res stream more than once (ex: two calls to res.json()).

2 Likes

@nmansilla I tried, but no luck.
Indeed, useEffect went from being executed twice to be executed only once, returning only status 200. However, for some reason, I still got a TypeError: body used already for: https://auth.atlassian.com/oauth/token error.

Before

Resolved by removing console.logs, I am not sure why though… It worked finally.

After

    .then((res) => res.json())
1 Like