hero icon indicating copy to clipboard operation
hero copied to clipboard

Cannot find context with specified id"

Open eNcacz opened this issue 1 year ago • 5 comments

I already reported this error as issue 263. The testcase from that issue now works well in 2.0.0-alpha.29, but the error still appears in my project. So I prepared another testcase which is failing in 2.0.0-alpha.29.

We need these two html files:

browserFrameTestParent.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Scraper test page for Browser frame methods</title>
</head>
<body>
    <h1>Main Page</h1>
    <div id="parentContent" >This is parent content</div>
    <iframe id="testFrame" class="frameClass" src="browserFrameTestFrame.html"></iframe>
</body>
</html>

browserFrameTestFrame.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Scraper test page for Browser frame methods</title>
</head>
<body>
    <h2>This is the Frame Page 1</h2>
    <a id="changeTopDocument" href="doesntMatterFile.html" target="_top">Change whole page, not only the frame.</a>
</body>
</html>

And this is the test script:

import Hero from '@ulixee/hero-playground'

process.on('unhandledRejection', (reason, _) => {
    if (reason instanceof Error)
        console.error(`Unhandled Rejection: ${reason}`, {stack: reason.stack})
    else
        console.error(`Unhandled Rejection: ${reason}`)
    process.exit(1)
});

(async () => {
    const hero = new Hero()
    await hero.goto('http://localhost/~vaclav/ulixee/browserFrameTestParent.html')  // <- change this url
    await hero.waitForPaintingStable()

    const frameElem = await hero.document.getElementById("testFrame")
    const frameEnv = await hero.mainFrameEnvironment.getFrameEnvironment(frameElem)
    const anchor = await frameEnv.document.getElementById("changeTopDocument")
    await anchor.scrollIntoView({block: 'end', inline: 'nearest'})
    await anchor.$click('none')

    console.log('Now it will crash')
    await anchor.isConnected

    console.log('This will not be printed. The application crash on the line above.')
    await hero.close()
    console.log('Done')
})();

When I run the script, then the output looks like this:

Started Ulixee Cloud at localhost:1818
Now it will crash
2024-07-19T11:01:41.004Z ERROR [hero-core/connections/ConnectionToHeroClient] ConnectionToClient.HandleRequestError {
  context: {},
  sessionId: '_qlBQuMWzC2wG9hgv_noX',
  sessionName: undefined
} ProtocolError: Runtime.callFunctionOn: Cannot find context with specified id
  at new Resolvable (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/Resolvable.ts:19:18)
    at createPromise (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/utils.ts:147:10)
    at DevtoolsSession.send (/home/vaclav/sandbox/ulixee/agent/main/lib/DevtoolsSession.ts:82:37)
    at Frame.evaluateOnNode (/home/vaclav/sandbox/ulixee/agent/main/lib/Frame.ts:489:49)
    at Frame.getContainerOffset (/home/vaclav/sandbox/ulixee/agent/main/lib/Frame.ts:425:47)
    at async JsPath.exec (/home/vaclav/sandbox/ulixee/agent/main/lib/JsPath.ts:69:29)
    at async FrameEnvironment.execJsPath (/home/vaclav/sandbox/ulixee/node_modules/core/lib/FrameEnvironment.ts:246:12)
    at async CommandRecorder.runCommandFn (/home/vaclav/sandbox/ulixee/node_modules/core/lib/CommandRecorder.ts:90:16)
    at async CommandRunner.runFn (/home/vaclav/sandbox/ulixee/node_modules/core/lib/CommandRunner.ts:36:14)
    at async ConnectionToHeroClient.executeCommand (/home/vaclav/sandbox/ulixee/node_modules/core/connections/ConnectionToHeroClient.ts:258:12) {
  method: 'Runtime.callFunctionOn',
  remoteError: { code: -32000, message: 'Cannot find context with specified id' }
}
Unhandled Rejection: ProtocolError: Runtime.callFunctionOn: Cannot find context with specified id {
  stack: '\n' +
    '\n' +
    '  --->  await anchor.isConnected\n' +
    '\n' +
    '\n' +
    'ProtocolError: Runtime.callFunctionOn: Cannot find context with specified id\n' +
    '  at new Resolvable (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/Resolvable.ts:19:18)\n' +
    '    at createPromise (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/utils.ts:147:10)\n' +
    '    at DevtoolsSession.send (/home/vaclav/sandbox/ulixee/agent/main/lib/DevtoolsSession.ts:82:37)\n' +
    '    at Frame.evaluateOnNode (/home/vaclav/sandbox/ulixee/agent/main/lib/Frame.ts:489:49)\n' +
    '    at Frame.getContainerOffset (/home/vaclav/sandbox/ulixee/agent/main/lib/Frame.ts:425:47)\n' +
    '    at async JsPath.exec (/home/vaclav/sandbox/ulixee/agent/main/lib/JsPath.ts:69:29)\n' +
    '    at async FrameEnvironment.execJsPath (/home/vaclav/sandbox/ulixee/node_modules/core/lib/FrameEnvironment.ts:246:12)\n' +
    '    at async CommandRecorder.runCommandFn (/home/vaclav/sandbox/ulixee/node_modules/core/lib/CommandRecorder.ts:90:16)\n' +
    '    at async CommandRunner.runFn (/home/vaclav/sandbox/ulixee/node_modules/core/lib/CommandRunner.ts:36:14)\n' +
    '    at async ConnectionToHeroClient.executeCommand (/home/vaclav/sandbox/ulixee/node_modules/core/connections/ConnectionToHeroClient.ts:258:12)\n' +
    '------REMOTE CORE---------------------------------\n' +
    '  at Function.reviver (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/TypeSerializer.ts:251:26)\n' +
    '    at JSON.parse (<anonymous>)\n' +
    '    at Function.parse (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/TypeSerializer.ts:31:17)\n' +
    '    at WsTransportToCore.onMessage (/home/vaclav/sandbox/ulixee/node_modules/net/lib/WsTransportToCore.ts:105:36)\n' +
    '    at WebSocket.emit (node:events:517:28)\n' +
    '    at Receiver.receiverOnMessage (/home/vaclav/sandbox/ulixee/node_modules/ws/lib/websocket.js:1220:20)\n' +
    '    at Receiver.emit (node:events:517:28)\n' +
    '    at Receiver.dataMessage (/home/vaclav/sandbox/ulixee/node_modules/ws/lib/receiver.js:596:14)\n' +
    '    at /home/vaclav/sandbox/ulixee/node_modules/ws/lib/receiver.js:530:12\n' +
    '    at /home/vaclav/sandbox/ulixee/node_modules/ws/lib/permessage-deflate.js:309:9\n' +
    '------CONNECTION----------------------------------\n' +
    '  at new Resolvable (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/Resolvable.ts:19:18)\n' +
    '    at createPromise (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/utils.ts:147:10)\n' +
    '    at PendingMessages.create (/home/vaclav/sandbox/ulixee/node_modules/net/lib/PendingMessages.ts:47:44)\n' +
    '    at ConnectionToHeroCore.sendRequest (/home/vaclav/sandbox/ulixee/node_modules/net/lib/ConnectionToCore.ts:158:50)\n' +
    '    at async CoreCommandQueue.sendRequest (/home/vaclav/sandbox/ulixee/node_modules/client/lib/CoreCommandQueue.ts:317:12)\n' +
    '    at async Object.cb (/home/vaclav/sandbox/ulixee/node_modules/client/lib/CoreCommandQueue.ts:231:16)\n' +
    '    at async Queue.next (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/Queue.ts:188:19)\n' +
    '------CORE COMMANDS-------------------------------\n' +
    '    at Queue.run (/home/vaclav/sandbox/ulixee/node_modules/commons/lib/Queue.ts:63:19)\n' +
    '    at CoreCommandQueue.run (/home/vaclav/sandbox/ulixee/node_modules/client/lib/CoreCommandQueue.ts:220:8)\n' +
    '    at CoreFrameEnvironment.execJsPath (/home/vaclav/sandbox/ulixee/node_modules/client/lib/CoreFrameEnvironment.ts:80:36)\n' +
    '    at execJsPath (/home/vaclav/sandbox/ulixee/node_modules/client/lib/SetupAwaitedHandler.ts:160:26)\n' +
    '    at Object.getProperty (/home/vaclav/sandbox/ulixee/node_modules/client/lib/SetupAwaitedHandler.ts:43:24)\n' +
    '    at async AwaitedHandler.getProperty (/home/vaclav/sandbox/ulixee/node_modules/files/2-finalized/awaited-dom/base/AwaitedHandler.ts:25:12)\n' +
    '    at async /home/vaclav/sandbox/ulixee/app.ts:23:5\n' +
    '\n' +
    '--------------------------------------------------\n' +
    '--------------------------------------------------\n' +
    '------_qlBQuMWzC2wG9hgv_noX-----------------------\n' +
    '--------------------------------------------------'
}

Process finished with exit code 1

eNcacz avatar Jul 19 '24 11:07 eNcacz

I hope you’re doing well. Would you be able to find some time to look into the reported issue and work on a fix? Your assistance would be greatly appreciated.

eNcacz avatar Nov 18 '24 13:11 eNcacz

@eNcacz I'd love to find time to take a look at this but my plate is still quite full. If you have any ability to wire up a unit test like we did for the last one and see where it's breaking (it will be somewhere in agent/lib/FramesManager or Frame), that will be your best option for a quicker resolution

blakebyrnes avatar Nov 18 '24 15:11 blakebyrnes

@blakebyrnes I looked into the problem and found the cause in the Frame in @ulixee/unblocked-agent.

It is caused by:

  public async getContainerOffset(): Promise<IPoint> {
    // This gets called on .isConnected() and this.getBoundingClientRect(); causes problem
    // if context changes and node with the frameElementNodeId no longer exists.
    // It can happen besause this.getFrameElementDevtoolsNodeId(); gets the real node id once
    // and on every subsequent call it returns the cached value.
    if (!this.parentId) return { x: 0, y: 0 };
    const parentOffset = await this.parentFrame.getContainerOffset();
    const frameElementNodeId = await this.getFrameElementDevtoolsNodeId();
    const thisOffset = await this.parentFrame.evaluateOnNode<IPoint>(
      frameElementNodeId,
      `(() => {
      const rect = this.getBoundingClientRect();
      return { x:rect.x, y:rect.y };
 })()`,
    );
    return {
      x: thisOffset.x + parentOffset.x,
      y: thisOffset.y + parentOffset.y,
    };
  }

and can be solved with change in evaluateOnNode:

public async evaluateOnNode<T>(devtoolsObjectId: string, expression: string): Promise<T> {
  if (this.closedWithError) throw this.closedWithError;
  try {
    // PROPOSED SOLUTION - Test if the node is still there, 
    // if it is not the Runtime.callFunctionOn will throw an unhandled error.
    // Other solution would be test this in getFrameElementDevtoolsNodeId() and throw error if it is not there.
    await this.parentFrame.devtoolsSession.send('DOM.getFrameOwner', { frameId: this.id }, this);

    const result = await this.devtoolsSession.send(
      'Runtime.callFunctionOn',
      {
        functionDeclaration: `function executeRemoteFn() {
      return ${expression};
    }`,
        returnByValue: true,
        objectId: devtoolsObjectId,
      },
      this,
    );
    if (result.exceptionDetails) {
      throw ConsoleMessage.exceptionToError(result.exceptionDetails);
    }

    const remote = result.result;
    if (remote.objectId) this.devtoolsSession.disposeRemoteObject(remote);
    return remote.value as T;
  } catch (err) {
    if (err instanceof CanceledPromiseError) return;
    throw err;
  }
}

kumbalek avatar Jan 10 '25 11:01 kumbalek

@kumbalek Nice! Did you test that this works? Or just think this is probably going to fix it?

blakebyrnes avatar Jan 14 '25 00:01 blakebyrnes

@blakebyrnes I re-tested it and found better solution that works and fixes @eNcacz problem.

JsPath.ts

  public async exec<T>(jsPath: IJsPath, timeoutMs?: number): Promise<IExecJsPathResult<T>> {
    await this.frame.navigationsObserver.waitForLoad(LoadStatus.JavascriptReady, {
      timeoutMs: timeoutMs ?? 30e3,
      doNotIncrementMarker: true,
    });

    try {
      const containerOffset = await this.frame.getContainerOffset();

      return this.runJsPath<T>(`exec`, jsPath, containerOffset);
    } catch (error) {
      if (jsPath.at(-1) === 'isConnected') {
        this.logger.warn('Error in getting this.frame.ContainerOffset()', {
          error,
        });

        return Promise.resolve({ value: false } as any);
      }

      throw error;
    }
  }

This catches the context error in getContainerOffset(); and if it is request for isConnected it returns false indicating it is not connected.

To be polished:

  • I dont know how the jsPath might look like, I expect isConnected method at the end of the array, but if it can be somewhere else adjust the condition to if (jsPath.includes('isConnected'))
  • You can add additional check in the condition for the err.code === ContextNotFoundCode so that this fix will apply only on this specific problem and not any other error that might arise in this.frame.ContainerOffset(). But I assumed if there is an error, isConnected should return false.

kumbalek avatar Jan 15 '25 00:01 kumbalek