Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save duongphuhiep/a3ed34c7bbd49199337a234e7b48a0d5 to your computer and use it in GitHub Desktop.
Save duongphuhiep/a3ed34c7bbd49199337a234e7b48a0d5 to your computer and use it in GitHub Desktop.
How to perform Request Reply communication between Iframe & Host

Problem

Your HTML page (the host page) has an Iframe. The Iframe source is on other domain, so you will have to use the window.postMessage() for cross-origin communication. But this communication is only in 1 direction (The Iframe can inform the Host or the Host can inform the Iframe)

This article shows you how to make the Request/Reply communication using postMessage() and MessageChannel combination.

The article did not show you how to add a timeout to the communication, so the Iframe might wait forever if the Host did not response.

I will resume the technique by some codes snippets and at the same time add the missing timeout implementation.

Codes snippets

/**
 * Send the request Cross-Origin and wait for a response
 * @param targetWindow: the window we want to broadcast the request to (usually `parent` or `iframe.contentWindow`)
 * @param request
 * @param timeoutMs: default 10000
 * @param targetOrigin: default '*'
 * @returns promise on response
 */
export function requestCrossOrigin<TRequest, TResponse>(
    targetWindow: Window,
    request: TRequest,
    timeoutMs?: number,
    targetOrigin?: string
): Promise<TResponse> {
    //apply this https://advancedweb.hu/how-to-use-async-await-with-postmessage/
    const mainPromise = new Promise<TResponse>((res, rej) => {
        const channel = new MessageChannel();

        //setup listener to the response
        channel.port1.onmessage = ({ data }) => {
            channel.port1.close();
            const response = data as TResponse | Error;
            if (response instanceof Error) {
                rej(response);
            } else {
                res(response);
            }
        };

        targetOrigin = targetOrigin ?? '*';

        //send the request
        targetWindow.postMessage(request, targetOrigin, [channel.port2]);
    });

    if (!timeoutMs || timeoutMs < 0) timeoutMs = 10000;
    return Promise.race([
        mainPromise,
        new Promise<TResponse>((_, reject) =>
            setTimeout(
                () =>
                    reject(
                        new Error(
                            `Timeout: Unable to get response within ${timeoutMs} ms`
                        )
                    ),
                timeoutMs
            )
        ),
    ]);
}

/**
 * Send response for requests coming from the requestCrossOrigin() function.
 * It is recommended to verify the `event.origin` before calling this function to make sure that the origin of the request is authorized.
 * For eg: `window.addEventListener('message', (event) => { if (event.origin==="https://myapp.com") replyCrossOrigin(event, myResponse) })`;
 * @param event the event object when the request is coming
 * @param response the response you want to send back
 */
export function replyCrossOrigin<TRequest, TResponse>(event: MessageEvent<TRequest>, response: TResponse | Error) {
    event.ports[0].postMessage(response);
}
@jpc-ae
Copy link

jpc-ae commented Dec 25, 2024

You can change

            setTimeout(
                () =>
                    reject(
                        new Error(
                            `Timeout: Unable to get response within ${timeoutMs} ms`
                        )
                    ),
                timeoutMs
            )

to

            setTimeout(
                reject,
                timeoutMs,
                new Error(
                    `Timeout: Unable to get response within ${timeoutMs} ms`
                )
            )

since reject is already a function, and the setTimeout function passes any arguments after timeoutMs to whatever function is marked. setTimeout is funny that way. Also, is there a reason you use res/rej in the first promise, but reject in the timeout one? It's usually nice to stay consistent (I'm partial to keeping the full names). Outside of that, nice reference.

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