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 viaapp.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
- Is the host frontend expected to send an empty resolver bootstrap (
entryPoint:"resolver",call:{}) to a remote resolver endpoint that has noroute.path? This lands asPOST /on the remote base URL. - 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 — a200 application/json {}causes the host to keep re-firingPOST /and never proceed to data calls. - Why does a failed/empty bootstrap cascade so that all subsequent
invokeRemotedata calls fail and never reach the backend? - Was there a platform change around Jun 2026 that introduced this bootstrap? Is this a known regression, and is there an official workaround?
- Is a
route.pathnow required on UI remote-resolver endpoints? (The manifest reference currently says it is not, for UI modules.)
What we have tried
- Added
POST /returning200 application/json {}→ host loops onPOST /, app still non-functional (State B). - Did not add a
route.pathto the resolver endpoint, permanifest-reference/endpoint(“UI module remote resolver endpoint paths are always specified in invokeRemote requests”).