Unclear Forge Remote `invokeRemote` response semantics

The invokeRemote documentation does not explain how invokeRemote handles the HTTP response.

From what I understand, invokeRemote takes a description of the HTTP request that should be made against the remote, but what happens to the response of that HTTP request?
The documented type signature suggests that the response body can be missing or must be an object.

function invokeRemote(
  input: InvokeRemoteInput
): Promise<{ [key: string]: any } | void>;

The documentation leaves me with a lot of questions:

  • How exactly is the response handled?
  • What if the server responds with an error response? If, in this case, the Promise rejects, what does it reject (and on which status codes)?
  • Does that caller have access to HTTP headers or HTTP status code?
  • Can the remote return an array or a plain value instead of an object? If not, why not?

It seems invokeRemote makes some choices about how it handles the HTTP response from the remote, which are not documented.

3 Likes

I just checked the actual Typescript definition in the latest @forge/bridge, which looks as follows:

export declare const invokeRemote: <T>(input: InvokeRemoteInput) => Promise<T>;

This suggests the response can be anything. In reality, it is probably anything accepted by JSON.parse but that’s a guess.

Hi @tbinna, thanks for the feedback, we will definitely look at improving the documentation. In terms of the questions that you have raised:

How exactly is the response handled?

Under the hood, invokeRemote makes a call to Forge; Forge then performs some validation, hydrates the request with OAuth tokens (if configured) and the FIT… etc. before proxying the request to your remote server. The response is then validated by Forge before responding back to the Front end, where invokeRemote will massage the response somewhat (Forge operates using GraphQL, so essentially invokeRemote is attempting to bridge between Forge’s GraphQL request/response).

What if the server responds with an error response? If, in this case, the Promise rejects, what does it reject (and on which status codes)?

Yes, the Promise returned by invokeRemote will reject. It will reject on all non-2XXs as noted here, but also if the remote returns a response that is not JSON.

Does that caller have access to HTTP headers or HTTP status code?

At this point, the caller does not have access to the HTTP status code. For successful requests, you will have access to the headers though (they will be under the headers prop of the resolved Promise)

Can the remote return an array or a plain value instead of an object? If not, why not?

In terms of the response, the content-type header must be application/json and we must be able to call JSON.parse() on the response (as you have guessed).

3 Likes

Thank you for the detailed response, @BoZhang.

I am still not entirely clear on the actual response payload shape.

At this point, the caller does not have access to the HTTP status code. For successful requests, you will have access to the headers though (they will be under the headers prop of the resolved Promise)

From your reply above, it sounds like the response shape is something like

{
  "headers": ...,
  ? // "body":
}

I am trying to get an invokeRemote example working so I can test this out myself but I am struggling with this: invokeRemote throws Error: Resolver has no definition for 'undefined'

Yup, the shape of the response should be as you’ve mentioned. I will have a look at your other post soon.

1 Like

@BoZhang I have been working with invokeRemote a bit more and wanted to add that it would be good if the documentation could also clearly define the error response semantics and the API could be improved.

If the remote returns an error response, the underlying GraphQL response payload looks something like this:

{
    "data": {
        "invokeExtension": {
            "success": false,
            "response": null,
            "contextToken": null,
            "errors": [
                {
                    "message": "Invalid response from remote",
                    "extensions": {
                        "errorType": "INVALID_REMOTE_INVOCATION_ERROR",
                        "statusCode": 400,
                        "__typename": "GenericMutationErrorExtension"
                    },
                    "__typename": "MutationError"
                }
            ],
            "__typename": "InvokeExtensionResponse"
        }
    },
    "extensions": {
        "gateway": {
            "request_id": "fb358c3eb9ad448bb024ca1bb6f9c0bb",
            "crossRegion": false,
            "edgeCrossRegion": false
        }
    }
}

As shown above, the errors array does expose the error code, but the caller is never passed on that information, which is quite annoying.

The invokeRemote interface is quite messy. It suggests to the caller that it has an HTTP interface, but the actual implementation is neither here nor there. It would be good if the API implementation and interface propagated all the details of the underlying HTTP call to the invokeRemote caller, including code, body, and headers for error responses.

The current workaround could be implementing the Forge Remote endpoint more like a GraphQL endpoint and always returning 200 responses, with all the details (incl. errors) in the response body.

Hi @tbinna ,

Thanks for sharing your feedback. We discussed error handling earlier and some of its shortcomings, particularly the fact that the status code, along with the headers and body are not available if there is an error.

The error you are seeing with the GQL response payload is actually the response from an internal Forge service. This service handles errors from the response it gets from the remote server and does some of its own internal error mapping, so we will need to do some work to propagate the error some other way. I have created this FRGE ticket, to track it.

1 Like