Is the Custom UI bridge `requestJira`/`requestConfluence` Typescript interface correct?

I am developing a Custom UI and have been trying to call the requestJira function provided by @forge/bridge with an abort signal which keeps failing at runtime with the following error:

Failed to execute 'fetch' on 'Window': Failed to read the 'signal' property from 'RequestInit': Failed to convert value to 'AbortSignal'.

The code looks something like this:

import {requestJira} from '@forge/bridge';

const controller = new AbortController();
requestJira(`/rest/api/3/project/search`, {
              method: 'GET',
              headers: ...,
              body,
              signal: controller.signal,
            })

It seems when this request is passed through the bridge call, something goes wrong with the signal property. If I remove the signal property, it works just fine. From what I understand, the requestJira/requestConfluence API the user calls is just a simulated “fetch” call. Under the hood, the input is converted into a GraphQL request, which makes me wonder if the underlying API should attach the signal to the GraphQL request to allow us to abort it.

The signal property is probably unsupported, but it would be nice if this could be correctly represented in the types. Currently, the Typescript types tell that all standard fetch options are accepted/supported.

As a side note, passing a signal property to requestJira/requestConfluence from @forge/api works. From what I can tell, in this case, it is just calling the node-fetch API.

2 Likes

Same here. Can’t use signal property with requestJira… hence no way to actually abort requests that are in flight.

1 Like

Hi team, can someone look at this ? this has become a major prblem for our app now. requestJira is supposed to be compatible with fetch API, however abort signals are not supported.

1 Like

Bump :slight_smile:

Facing same issue here.

From what I understand, the requestJira /requestConfluence API the user calls is just a simulated “fetch” call.

Yes, the UI iframe’s fetch() sends a message to the parent frame with postMessage(), which in turn has the following strategies to perform requests:

  • requestJira(), requestConfluence() and requestBitbucket() will fetch Atlassian’s API directly in the frontend
  • invoke() will fetch the backend’s handler/resolver configured in manifest.yml
  • invokeRemote() will fetch through the backend to the remote configured in manifest.yml

The parent frame then sends a message to the inner frame with postMessage() with the outcome of the request.

The way these messages are posted, through the post-robot library, actually allows async function calls between frames. So, your signal is stringified in the inner frame into a special JSON that encodes its functions, and it’s parsed in the parent frame into an object with async functions that, on invocation, will postMessage() a special message to the inner frame. This should be happening for throwIfAborted(), addEventListener(), removeEventListener() and dispatchEvent().

However, these methods are not async, and as such they are never awaited or then()ed, so this doesn’t really work; for instance, throwIfAborted() can’t perform its side-effects synchronously on the parent frame. Also, properties become fields, so signal.aborted and signal.onabort don’t work on the parent frame, although my guess is that they’re not used.

Moreover, fetch() and other Promise-based native- or host-objects APIs require an internal implementation of AbortSignal where, among other things, they can add abort algorithms which happen before and independently of the abort event, so you can’t provide a duck-typed signal. So, all that post-robot trickery happens for nothing in this case.

This inter-frame barrier makes signals unsupported.

If only post-robot or @forge/bridge could “massage” it:

  • in the parent frame, create an AbortController and use its .signal instead of the parsed object
  • post-robot invoke, from parent frame to inner frame, signal.addEventListener('abort', e => abortController.abort(e.target.reason))
1 Like