App and I get different responses to the same successful API call

I’m developing an app with a space page, so I built it on a development site and just now installed it on the real site, where (naturally) I’m seeing new bugs.

The app is trying to call the /wiki/api/v2/spaces?key=SPACEKEY endpoint to get from its space key (given in the context) to its space ID (needed for further API calls).

When I visit https://MY-SITE.atlassian.net/wiki/api/v2/spaces?key=SPACEKEY in a browser, I get the expected response (a JSON blob with one result containing space information). When the app tries to reach the same URL, it gets a successful response (status code 200) whose results array is empty. There’s no permission request or any other kind of error, and it works fine on my development site. On the live site only, it successfully returns nothing.

I’ve verified with logging that it’s hitting the endpoint I expect, so either there’s something wrong with the base URL it’s using, or some difference in configuration between the two sites is keeping it from retrieving the expected information.

What can I check next?

Here’s a summary version of the code the app is using to call the endpoint:

      // const nextEndpoint = route`/wiki/api/v2/spaces?key=${context.spaceKey}`;
      const response = await api.asApp().requestConfluence(nextEndpoint, {
        headers: {
          'Accept': 'application/json'
        }
      });
      if (response.status != 200) {
        console.error(`Failed to resolve ${nextEndpoint}:`, response);
        nextEndpoint = undefined;
      } else {
        const json = await response.json();
        if (!json.results?.length) {
          console.warn('Query succeeded but got no results:', json);
        }
      // return json.results;

In the log, I see “Query succeeded but got no results,” and this JSON: { "results": [], "_links": {} }. The app in Confluence doesn’t show any errors, it just continues running without the missing data.

In case it’s useful, here’s the real hook that excerpt is part of, which includes a bunch of other logic because I also use it for API calls that need paging:

export const useAPI = (endpointRoute) => {
  const [responseData, setResponseData] = useState([]);
  useEffect(async () => {
    const allResults = [];
    let nextEndpoint = endpointRoute;
    while (nextEndpoint) {
      const response = await api.asApp().requestConfluence(nextEndpoint, {
        headers: {
          'Accept': 'application/json'
        }
      });
      if (response.status != 200) {
        console.error(`Failed to resolve ${nextEndpoint}:`, response);
        nextEndpoint = undefined;
      } else {
        const json = await response.json();
        if (!json.results?.length) {
          console.warn('Query succeeded but got no results:', json);
        }
        allResults.push(...json.results);
        const nextLink = response.headers.get('link')?.match('<([^>]*)>')[1];
        if (nextLink) {
          console.log(`Collected data for ${nextEndpoint.value_}, ${allResults.length} results so far.`);
          nextEndpoint = assumeTrustedRoute(nextLink);
        } else {
          console.log(`Finished collecting data for ${endpointRoute.value_}, ${allResults.length} total results.`);
          nextEndpoint = undefined;
        }
      }
    }
    setResponseData(allResults);
  }, [endpointRoute]);
  return responseData;
}

And I call it like this:

export const spaceRoute = (spaceKey) => {
  return spaceKey ? route`/wiki/api/v2/spaces?keys=${spaceKey}&limit=250` : undefined;
}

export const spacePagesRoute = (spaceId) => {
  return spaceId ? route`/wiki/api/v2/spaces/${spaceId}/pages?status=current&limit=250` : undefined;
}

export const usePages = (spaceKey) => {
  const spaceList = useAPI(spaceRoute(spaceKey));
  // ^-- this call is just to get the ID to use in this call --v
  const allPages = useAPI(spacePagesRoute(spaceList[0]?.id));
  // ... do stuff with page data ...
}

@FinnEllis,

I think I have a clue (not quite an answer). When you use the browser to call the API, that’s an asUser call, while your app makes an asApp call. I don’t know what your app does, but maybe you would want to make the request asUser so you don’t leak content that users shouldn’t see?

I’ll give it a shot, but … why would it work in the test space then? (And why would users not be able to see information about the space they’re authenticated to?)

@FinnEllis,

Good questions. That’s why my clue isn’t an answer. It could be the configuration of prod, like permissions. But I think you indicated earlier that you were successful in making a copy, which would largely bring over permissions.

But this part:

The asApp mode doesn’t look at user permissions, it just looks at its own. So, if the app has somehow been excluded from a Space, then it can’t see anything. The asUser mode is the way you let user permissions decide that.

Ah, hmm. That makes sense – and now that you say that I remember that I did have to update permissions on the imported space to let the app see it, I should have taken note of that at the time. :sweat_smile: This also means the app can’t show the user information about pages it can see but the user for some reason can’t, which seems like the right behavior.

And switching to asUser() worked! Thanks.

1 Like