Skip to content

[APPS-2947] Fix: let a framed backend call wait out the server's 5-minute run limit - #624

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
masterfrom
tiffany.trinh/apps-2947-frame-timeout
Oct 27, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
masterfrom
tiffany.trinh/apps-2947-frame-timeout

Conversation

@tyffical

@tyffical tyffical commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

  • APPS-2947 Run Live Preview backend functions from the parent page: a framed app sends each backend function call to its parent page and waits for the reply.
  • The frame gave up after 2 minutes, but the server lets one run go for up to 5 minutes, so a long call failed in the frame while the run was still in flight.
  • The frame now waits 30s past the server's 5-minute ceiling, which covers forwarding the call and its response, so the server is always the binding limit.

Architecture

sequenceDiagram
    participant F as Framed app (postMessageTransport)
    participant P as Parent page
    participant S as Backend function run
    F->>P: app-builder:run-query
    Note over F: waits up to POSTMESSAGE_TIMEOUT_MS (now 5 min)
    P->>S: run the function
    Note over S: server stops any run after 5 min
    S-->>P: result or error
    P-->>F: app-builder:run-query:response
Loading
  • The dev client hook's give-up backstop shares POSTMESSAGE_TIMEOUT_MS and rises with it; the hook's own ping (2s) and bundle-fetch (60s) timers still bound it.

Changes

2 changes across post-message-transport.ts, post-message-transport.test.ts
What changed File
Raise POSTMESSAGE_TIMEOUT_MS from 120_000 to 330_000. post-message-transport.ts
Add a test that a run ending at the server limit, with 10s of forwarding and response delay, resolves the call. post-message-transport.test.ts

QA Instructions

  • All 3 checks pass at dec69b75782e83e7fdae62bddf3e2bc02f042400.
Check Result Evidence
Real Chromium, cross-origin frame, parent replies after 150s, master source Rejects with "timed out waiting for response" at 120,004 ms Harness output below
Same, this branch's source Resolves "late-ok" at 150,011 ms Harness output below
New unit test against master's 120s value Fails with the timeout error Local run before the constant change
# From the repo root.
yarn workspace @dd/tests test:unit packages/plugins/apps/src/backend/client/transports/post-message-transport
# Expected: 27 passed
node late-reply.mjs "$(pwd)" 150000
# Expected: master rejects at ~120000 ms; branch resolves "late-ok" at ~150000 ms
late-reply.mjs: frames the bundled transport in headless Chromium and answers late
// Usage: node late-reply.mjs <build-plugins checkout> [replyDelayMs]
// Frames a child page (127.0.0.1) in a parent page (localhost) in headless Chromium. The child calls
// postMessageTransport from the bundled source; the parent answers after replyDelayMs. Runs the
// origin/master source and the checkout's source side by side.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import { createRequire } from 'node:module';

const checkout = process.argv[2];
const replyDelayMs = Number(process.argv[3] ?? 150_000);
const require = createRequire(`${checkout}/package.json`);
const esbuild = require('esbuild');
const { chromium } = require('playwright');

const relPath =
    'packages/plugins/apps/src/backend/client/transports/post-message-transport/post-message-transport.ts';
const sourceDir = `${checkout}/${relPath.replace(/\/[^/]+$/, '')}`;

async function bundle(contents) {
    const result = await esbuild.build({
        stdin: { contents, resolveDir: sourceDir, sourcefile: 'post-message-transport.ts', loader: 'ts' },
        bundle: true,
        format: 'iife',
        globalName: 'Transport',
        platform: 'browser',
        write: false,
    });
    return result.outputFiles[0].text;
}

const sources = {
    master: execFileSync('git', ['-C', checkout, 'show', `origin/master:${relPath}`]).toString(),
    branch: fs.readFileSync(`${checkout}/${relPath}`, 'utf8'),
};
const bundles = {};
for (const [name, source] of Object.entries(sources)) {
    bundles[name] = await bundle(source);
    const timeout = source.match(/POSTMESSAGE_TIMEOUT_MS = ([\d_]+)/)[1];
    console.log(`${name}: POSTMESSAGE_TIMEOUT_MS = ${timeout}`);
}

const childPage = (variant) => `<!doctype html><script>${bundles[variant]}</script><script>
const start = performance.now();
Transport.postMessageTransport('backend/slow.run', []).then(
    (data) => { window.outcome = { status: 'resolved', data, ms: Math.round(performance.now() - start) }; },
    (error) => { window.outcome = { status: 'rejected', message: error.message, ms: Math.round(performance.now() - start) }; },
);
</script>`;

const parentPage = (variant, childPort) => `<!doctype html><script>
window.addEventListener('message', (event) => {
    if (event.data?.type !== 'app-builder:run-query') return;
    setTimeout(() => event.source.postMessage({
        type: 'app-builder:run-query:response',
        requestId: event.data.requestId,
        success: true,
        result: { data: 'late-ok' },
    }, event.origin), ${replyDelayMs});
});
</script><iframe src="http://127.0.0.1:${childPort}/child?variant=${variant}"></iframe>`;

function serve(handler) {
    return new Promise((resolve) => {
        const server = http.createServer((req, res) => {
            const url = new URL(req.url, 'http://x');
            res.setHeader('Content-Type', 'text/html');
            res.end(handler(url));
        });
        server.listen(0, () => resolve(server));
    });
}

const childServer = await serve((url) => childPage(url.searchParams.get('variant')));
const childPort = childServer.address().port;
const parentServer = await serve((url) => parentPage(url.searchParams.get('variant'), childPort));
const parentPort = parentServer.address().port;

const browser = await chromium.launch();
const runs = Object.keys(bundles).map(async (variant) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto(`http://localhost:${parentPort}/parent?variant=${variant}`);
    const frame = page.frames().find((f) => f.url().includes('/child'));
    const handle = await frame.waitForFunction(() => window.outcome, null, {
        timeout: replyDelayMs + 60_000,
        polling: 1_000,
    });
    return [variant, await handle.jsonValue()];
});
for (const [variant, outcome] of await Promise.all(runs)) {
    console.log(`${variant}: parent replies after ${replyDelayMs}ms ->`, JSON.stringify(outcome));
}
await browser.close();
childServer.close();
parentServer.close();
  • Output at dec69b75782e83e7fdae62bddf3e2bc02f042400:
master: POSTMESSAGE_TIMEOUT_MS = 120_000
branch: POSTMESSAGE_TIMEOUT_MS = 300_000
master: parent replies after 150000ms -> {"status":"rejected","message":"Backend function \"backend/slow.run\" timed out waiting for response","ms":120004}
branch: parent replies after 150000ms -> {"status":"resolved","data":"late-ok","ms":150011}

Blast Radius

  • No feature flag; Low risk: one constant in the framed client.
  • A parent page that never answers now leaves a call pending for 5.5 minutes instead of 2 before the app sees the timeout error.

Documentation

🤖 Generated with Claude Code

…end call

A framed app gave up on a backend function call after 2 minutes, while
the server lets one run go for up to 5 minutes. A call that used more
than 2 minutes of its budget failed in the frame with a timeout while
the run was still in flight.

Raise POSTMESSAGE_TIMEOUT_MS to 300_000 so the frame is never the
binding limit. The dev client hook's give-up backstop shares the
constant and rises with it; the hook's own ping and bundle-fetch
timers still bound it at about a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical marked this pull request as ready for review October 27, 2026 15:23
@tyffical
tyffical requested a review from a team as a code owner October 27, 2026 15:23
@tyffical
tyffical requested review from a team, ksun154 and setnilson and a balanced review from Copilot and removed request for a team, ksun154 and setnilson October 27, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Matching the server execution limit leaves no allowance for forwarding and response delays, so valid near-limit runs can still time out prematurely.

1 open issue
What changed in this PR

Extends the framed backend transport’s response wait to five minutes to support longer App Builder Live Preview calls.

Changes:

  • Raises the shared timeout from 120,000 to 300,000 ms, also extending the dev-client hook’s fallback deadline.
  • Adds a regression test for a response arriving just before five minutes.
File Description
packages/​plugins/​apps/​src/​backend/​client/​transports/​post-message-transport/​post-message-transport.ts Increases the timeout and documents its intended relationship to the server limit.
packages/​plugins/​apps/​src/​backend/​client/​transports/​post-message-transport/​post-message-transport.test.ts Tests successful resolution for a late response.

Review effort: Balanced

@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T15:36:50.394386Z dec69b7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dec69b7578

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The frame starts its timer before the parent forwards the call, and the
response takes time to come back, so a run ending at the 5-minute server
limit used to arrive after the frame gave up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-27 22:07:22 UTC ℹ️ Start processing command /merge


2026-10-27 22:07:26 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2m (p90).


2026-10-27 22:08:48 UTC ℹ️ MergeQueue: This merge request was merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants