API v2 update page: code: 403, message: 'The app is not installed on this instance'

I am using a queue consumer to do a repair of some data of the page (basically a missing macro configuration property as the data type has changed)

For this, I first get the page with

  const res = await api
    .asApp()
    .requestConfluence(route`/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
      headers: {
        'Accept': 'application/json'
      }
    });

which works fine.

I update the body and try to update with (straight from the documentation):

    const response = await api.asApp().requestConfluence(route`/wiki/api/v2/pages/${payload.contentId}`, {
      method: 'PUT',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: pageWithBody
    });
    console.log(`Repair Response: ${response.status} ${response.statusText}`);
    console.log(await response.json());

but the result is:

INFO    2023-04-02T10:22:19.819Z 7c5efaa7-fd55-4a6f-9312-ae55b9e45758 Repair Response: 403 Forbidden
INFO    2023-04-02T10:22:19.819Z 7c5efaa7-fd55-4a6f-9312-ae55b9e45758 { code: 403, message: 'The app is not installed on this instance' }

The app is clearly installed on this instance, so this error is wrong. It seems to be a permissions issue (403), but I do not get an authentication warning (which I can force by using asUser() instead of asApp().

For completeness, here are the scopes from manifest.yml:

permissions:
  scopes:
    - read:confluence-content.summary
    - read:confluence-content.all
    - read:page:confluence
    - storage:app
    - read:space:confluence
    - read:confluence-props
    - write:confluence-props
    - write:confluence-content
    - write:confluence-space

I’ve created a version that calls the api asUser() instead of asApp() this is called directly from a button on a page. The error now becomes:

INFO    2023-04-02T13:05:47.461Z 9ed4dcfe-8c3d-47da-9124-141a2227cf29 Repair Response: 401 Unauthorized
INFO    2023-04-02T13:05:47.462Z 9ed4dcfe-8c3d-47da-9124-141a2227cf29 { code: 401, message: 'Unauthorized; scope does not match' }

Additionally. On install, I only see these scopes:

Your app will be installed with the following scopes:
- write:confluence-content
- write:confluence-space

Hi @GerbenWierda ,

To call PUT /wiki/api/v2/pages/{id}, your app needs an additional scope: write:page:confluence.

To elaborate, Confluence has 3 different sets of scopes:

  • Connect scopes
  • Classic OAuth 2.0 scopes
  • Granular OAuth 2.0 scopes

The Confluence V2 REST API uses only “granular scopes”, whilst the Confluence V1 REST API supports both classic and granular scopes.

Here is the code I created to validate this will work with the additional write:page:confluence scope:

import api, { route } from '@forge/api';
import { Queue } from '@forge/events';
import Resolver from "@forge/resolver";

const testQueue = new Queue({ key: 'test-updates' });

const testResolver = new Resolver();
testResolver.define("test-event-listener", async (queueItem) => {
  const eventPayload = queueItem.payload;
  const eventContext = queueItem.context;
  console.log(` * payload: ${JSON.stringify(eventPayload, null, 2)}`);
  console.log(` * context: ${JSON.stringify(eventContext, null, 2)}`);
  const pageId = eventPayload.pageViewEvent.content.id;
  console.log(` * pageId: ${pageId}`);
  const requestPageResponse = await api
    .asApp()
    .requestConfluence(route`/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
      headers: {
        'Accept': 'application/json'
      }
    });
  const requestPageData = await requestPageResponse.json();
  console.log(` requestPageData = ${JSON.stringify(requestPageData, null, 2)}`);

  // Stop processing if on any other page since the remaining processsing updates the page.
  if (pageId !== "1489207297") {
    return;
  }
  const pageVersion = requestPageData.version.number;
  console.log(` * pageVersion: ${pageVersion}`);
  const newPageBody = {
    representation: "storage",
    value: `Hello, the content of this page was completely replace at ${new Date().toISOString()}.`,
  };
  const body = {
    id: pageId,
    status: "current",
    title: requestPageData.title,
    spaceId: requestPageData.spaceId,
    body: newPageBody,
    version: {
      number: pageVersion + 1
    }
  }
  console.log(` body = ${JSON.stringify(body, null, 2)}`);
  const pageUpdateResponse = await api.asApp().requestConfluence(route`/wiki/api/v2/pages/${pageId}`, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });
  const pageUpdateData = await pageUpdateResponse.json();
  console.log(`Repair Response: ${pageUpdateResponse.status} ${pageUpdateResponse.statusText}`);
  console.log(` pageUpdateData = ${JSON.stringify(pageUpdateData, null, 2)}`);
});
export const testQueueHandler = testResolver.getDefinitions();

export async function dl_handleTriggerPageViewed(event, context) {
  console.log(`dl_handleTriggerPageViewed`);
  console.log(` * context: ${JSON.stringify(context)}`);
  await testQueue.push({
    pageViewEvent: event,
    pageViewedContext: context
  });
};

Regards,
Dugald

Thank you. Weirdly enough I am pretty certain that I tried this earlier (simply by copying the read line to a write line), but something else will have been wrong at the time.

One thing should probably be fixed in the documentation. There it says:

const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/{id}`, {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: bodyData
});

which gives me a 400 error with

INFO    2023-04-04T06:19:22.938Z 0a92716e-b1e5-4239-86da-c6c06c9f4c3f Repair Response: 400 Bad Request
INFO    2023-04-04T06:19:22.939Z 0a92716e-b1e5-4239-86da-c6c06c9f4c3f {
  errors: [
    {
      status: 400,
      code: 'INVALID_MESSAGE',
      title: 'Invalid message',
      detail: null
    }
  ]
}

What works is:

const response = await api.asUser().requestConfluence(route`/wiki/api/v2/pages/{id}`, {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(bodyData)
});

(The JSON.stringify() in your solution (but not in the example in the documentation) is required for me to make it work.)

I had already been wondering about race conditions. It seems you partly have that in mind with:

  if (pageId !== "1489207297") {
    return;
  }

Can you explain what is going on, especially because this is some hard coded number?

Hi @GerbenWierda ,

Since the code executes in response to page viewed events and updates the page with some meaningless content just to provide you an example, I didn’t want it to be re-writing other pages in my test tenant where I had this app installed so I just hard coded the page ID that I was testing this with so it would only continue an update this one page.

Regards,
Dugald