invokeRemote (frontend) breaks: empty resolver bootstrap POST "/" to remote root 404s and cascades to all data calls

Summary

We use Forge Remote with a Custom UI frontend calling invokeRemote({ path, method, body }). Our manifest follows the documented “Calling a remote backend from a Forge frontend” pattern exactly (resolver wired to a remote endpoint, no route.path).

It was working fine before, only in the last few days, the host frontend started auto-firing an empty resolver bootstrap on app load. It is proxied to our remote backend as POST / (User-Agent Forge) with no path. Our backend has no / route, so it returns 404 text/html (“Cannot POST /”). Immediately after, all invokeRemote data calls fail with:

Invalid response from remote — content-type header must be set to application/json
and is currently: text/html; charset=utf-8. Status: 404.

The data calls never reach our backend, our access log shows only the POST / requests, no /<api-prefix>/* requests. The app shell loads but is completely non-functional (no data).

This appears to affect the documented invokeRemote-from-frontend setup, so we suspect a recent platform-side behavior change / regression.

Environment

  • Forge Remote, operations: compute. Custom UI (Jira app, also a Connect-on-Forge hybrid via app.connect.remote).
  • @forge/bridge: <5.8.0 / 5.17.0>
  • Remote backend: Node/Express on Google App Engine (nodejs24).
  • Runtime: nodejs24.x.

Manifest (documented pattern - note: resolver endpoint has NO route.path)

modules:
  jira:globalPage:
    - key: <module-key>
      resolver:
        endpoint: remote-resolver        # documented UI remote-resolver wiring
  endpoint:
    - key: remote-resolver
      remote: remote-backend-key
      auth:
        appUserToken: { enabled: true }
        appSystemToken: { enabled: true }
      # no route.path — per manifest-reference/endpoint: "UI module remote resolver
      # endpoint paths are always specified in invokeRemote requests"
remotes:
  - key: remote-backend-key
    baseUrl: https://<our-remote-backend>
    operations:
      - compute
    auth:
      appUserToken: { enabled: true }
      appSystemToken: { enabled: true }

Frontend call (the only way we talk to the backend)

import { invokeRemote } from '@forge/bridge';
const res = await invokeRemote({
  path: '/<api-prefix>/v1/some-endpoint',
  method: 'GET',
  headers: { Accept: 'application/json' },
});

Reproduction / observed behavior

State A - no POST / handler (default): Backend access log on app load:

"POST / HTTP/1.1" 404 140 "-" "Forge"
"POST / HTTP/1.1" 404 140 "-" "Forge"
... (one per UI module iframe) ...

Browser console: every invokeRemote data call → Invalid response from remote — content-type ... text/html ... Status 404. No /<api-prefix>/* request ever reaches the backend.

State B - we added app.post('/', (req,res) => res.status(200).json({})) (a docs-compliant 2xx + application/json): POST / now returns 200, but it fires repeatedly and does not settle, and data calls still never reach the backend / the app still does not load. Sample log (≈4 seconds, ~9 requests, no /<api-prefix>/* lines at all):

"POST / HTTP/1.1" 200 2 "-" "Forge"
"POST / HTTP/1.1" 200 2 "-" "Forge"
"POST / HTTP/1.1" 200 2 "-" "Forge"
... repeats ...

So returning the documented contract (2xx + application/json) does not allow the relay to initialize.

Captured bootstrap request

We instrumented POST / to log the request. The bootstrap Forge sends to our remote root is (IDs/token redacted):

content-type  = application/json
user-agent    = Forge
authorization = present (Forge Invocation Token, Bearer)
body =
{
  "call": {},                               // EMPTY — no functionKey / payload / jobId
  "context": {
    "cloudId": "<cloudId>",
    "localId": "ari:cloud:ecosystem::extension/<appId>/<envId>/static/main",
    "environmentId": "<envId>",
    "environmentType": "STAGING",
    "moduleKey": "main",
    "siteUrl": "https://<your-site>.atlassian.net",
    "appVersion": "16.18.0",
    "extension": { "type": "jira:globalPage", "jira": { "isNewNavigation": true } }
  },
  "contextToken": "<JWT forge/context-token>"
}

Confirmed loop: with POST / → 200 application/json {}, the host re-fires POST / continuously (~1/sec, indefinitely), every response 200, and no /<api-prefix>/* request ever reaches the backend. So a docs-compliant 2xx + application/json body does NOT settle the relay. The empty call: {} indicates the host is bootstrapping the endpoint as a resolver, but our remote is a plain REST backend (no @forge/resolver), so it cannot return the resolver response the relay expects.

Questions

  1. Is the host frontend expected to send an empty resolver bootstrap (entryPoint:"resolver", call:{}) to a remote resolver endpoint that has no route.path? This lands as POST / on the remote base URL.
  2. What is the exact HTTP response contract the backend must return to this bootstrap so the relay initializes? The documented contract (2xx + Content-Type: application/json) is not sufficient — a 200 application/json {} causes the host to keep re-firing POST / and never proceed to data calls.
  3. Why does a failed/empty bootstrap cascade so that all subsequent invokeRemote data calls fail and never reach the backend?
  4. Was there a platform change around Jun 2026 that introduced this bootstrap? Is this a known regression, and is there an official workaround?
  5. Is a route.path now required on UI remote-resolver endpoints? (The manifest reference currently says it is not, for UI modules.)

What we have tried

  • Added POST / returning 200 application/json {} → host loops on POST /, app still non-functional (State B).
  • Did not add a route.path to the resolver endpoint, per manifest-reference/endpoint (“UI module remote resolver endpoint paths are always specified in invokeRemote requests”).

Hi @MaxWell, this is a strange one. Can you please raise a support request so that we can investigate further?

Thanks,
Bo