npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

request-iframe

v0.2.2

Published

Communicate with iframes like sending HTTP requests

Readme

request-iframe

Communicate with iframes/windows like sending HTTP requests! A cross-origin browser communication library based on postMessage.

🌐 Languages: English | 中文

📑 Table of Contents

Why request-iframe?

In micro-frontend, iframe nesting, and popup window scenarios, cross-page communication is a common requirement. Traditional postMessage communication has the following pain points:

| Pain Point | Traditional Way | request-iframe | |------------|----------------|----------------| | Request-Response Association | Manual requestId management | Automatic management, Promise style | | Timeout Handling | Manual timer implementation | Built-in multi-stage timeout mechanism | | Error Handling | Various edge cases | Standardized error codes | | Message Isolation | Easy to cross-talk | secretKey automatic isolation | | API Style | Event listener style | HTTP-like request/Express style | | TypeScript | Need custom types | Full type support | | Test Coverage | None | 76%+ test coverage |

Core Advantages:

  • Zero Learning Curve - If you're familiar with axios and Express, you can get started immediately
  • Type Safe - Full TypeScript support for a great development experience
  • Production Ready - High test coverage, thoroughly tested
  • Feature Rich - Interceptors, middleware, streaming, file transfer all included

Features

  • 🚀 HTTP-like Style - Client sends requests, Server handles and responds, just like axios + express
  • 🔌 Interceptor Support - Request/response interceptors for unified authentication, logging, etc.
  • 🎭 Middleware Mechanism - Express-style middleware with path matching support
  • ⏱️ Smart Timeout - Three-stage timeout (connection/sync/async), automatically detects long tasks
  • 📦 TypeScript - Complete type definitions and IntelliSense
  • 🔒 Message Isolation - secretKey mechanism prevents message cross-talk between multiple instances
  • 📁 File Transfer - File transfer via streams (client↔server)
  • 🌊 Streaming - Support for large file chunked transfer, supports async iterators
  • 🧾 Leveled Logging - Warn/Error logs enabled by default; can be configured via trace level
  • 🌍 Internationalization - Error messages can be customized for i18n
  • Protocol Versioning - Built-in version control for upgrade compatibility

Installation

npm install request-iframe
# or
yarn add request-iframe
# or
pnpm add request-iframe

Requirements: Node.js >= 14

TypeScript: Built-in complete type definitions, no need to install @types/request-iframe

CDN (UMD bundles)

This repo also builds script-tag friendly UMD bundles (core + react hooks) that can be hosted on a CDN.

  • Core bundle output: cdn/request-iframe.umd(.min).js → global RequestIframe
  • React bundle output: cdn/request-iframe-react.umd(.min).js → global RequestIframeReact (requires React global and RequestIframe global)

Example (using unpkg):

<!-- Core -->
<script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe.umd.min.js"></script>

<!-- React (optional) -->
<script src="https://unpkg.com/react@latest/umd/react.production.min.js"></script>
<script src="https://unpkg.com/request-iframe@latest/cdn/request-iframe-react.umd.min.js"></script>

<script>
  const { requestIframeClient, requestIframeServer, requestIframeEndpoint } = RequestIframe;
  // React hooks are available on RequestIframeReact (e.g. RequestIframeReact.useClient)
  console.log(!!requestIframeClient, !!requestIframeServer, !!requestIframeEndpoint);
<\/script>

Quick Start

1. Parent Page (Client Side)

import { requestIframeClient } from 'request-iframe';

/** Get iframe element */
const iframe = document.querySelector('iframe') as HTMLIFrameElement;

/** Prefer waiting iframe load so contentWindow is ready */
await new Promise<void>((resolve) => iframe.addEventListener('load', () => resolve(), { once: true }));

/**
 * Create client (safer + less boilerplate default)
 * - strict: true defaults targetOrigin/allowedOrigins to window.location.origin (same-origin only)
 * - For cross-origin, explicitly configure targetOrigin + allowedOrigins/validateOrigin
 */
const client = requestIframeClient(iframe, { secretKey: 'my-app', strict: true });

/** Send request (just like axios) */
const response = await client.send('/api/getUserInfo', { userId: 123 });
console.log(response.data); // { name: 'Tom', age: 18 }

2. iframe Page (Server Side)

import { requestIframeServer } from 'request-iframe';

/**
 * Create server
 * - Strongly recommended to configure allowedOrigins / validateOrigin in production
 * - This snippet assumes a same-origin demo (parent origin === iframe origin)
 *   For cross-origin, replace with the real parent origin (e.g. 'https://parent.example.com')
 */
const server = requestIframeServer({ secretKey: 'my-app', strict: true });

/** Register handler (just like express) */
server.on('/api/getUserInfo', (req, res) => {
  const { userId } = req.body;
  res.send({ name: 'Tom', age: 18 });
});

That's it! 🎉

💡 Tip: For more quick start guides, see QUICKSTART.md or QUICKSTART.CN.md (中文)

Which API should I use?

  • Use requestIframeClient() + requestIframeServer() when communication is mostly one-way (parent → iframe), and you prefer a clear separation between request sender and handler.
  • Use requestIframeEndpoint() when you need bidirectional communication (both sides need send() + on()/use()/map()), or when you want a single façade object to debug a full flow more easily.

Use Cases

Micro-Frontend Communication

In micro-frontend architecture, the main application needs to communicate with child application iframes:

/** Main application (parent page, same-origin default: strict: true) */
const client = requestIframeClient(iframe, { secretKey: 'main-app', strict: true });

// Get user info from child application
const userInfoResponse = await client.send('/api/user/info', {});
console.log(userInfoResponse.data); // User info data

// Notify child application to refresh data
await client.send('/api/data/refresh', { timestamp: Date.now() });

Third-Party Component Integration

When integrating third-party components, isolate via iframe while maintaining communication:

/** Parent page (same-origin default: strict: true) */
const client = requestIframeClient(thirdPartyIframe, { secretKey: 'widget', strict: true });

// Configure component
await client.send('/config', {
  theme: 'dark',
  language: 'en-US'
});

/** Listen to component events (via reverse communication) */
const server = requestIframeServer({ secretKey: 'widget', strict: true });
server.on('/event', (req, res) => {
  console.log('Component event:', req.body);
  res.send({ received: true });
});

If the third-party iframe is cross-origin, explicitly configure targetOrigin and allowedOrigins/validateOrigin (see the security section).

Popup / New Window (Window Communication)

request-iframe also works with a Window target (not only an iframe).

Important: you must have a real Window reference (e.g. returned by window.open(), or available via window.opener / event.source). You cannot send to an arbitrary browser tab by URL.

/** Parent page: open a new tab/window */
const child = window.open('https://child.example.com/page.html', '_blank');
if (!child) throw new Error('Popup blocked');

/** Parent -> child */
const targetOrigin = 'https://child.example.com';
const client = requestIframeClient(child, {
  secretKey: 'popup-demo',
  targetOrigin, // strongly recommended (avoid '*')
  allowedOrigins: [targetOrigin]
});
await client.send('/api/ping', { from: 'parent' });

/**
 * Child page: create server
 * Note: allowedOrigins should be the real parent origin (example value below).
 */
const parentOrigin = 'https://parent.example.com';
const server = requestIframeServer({ secretKey: 'popup-demo', allowedOrigins: [parentOrigin] });
server.on('/api/ping', (req, res) => res.send({ ok: true, echo: req.body }));

Cross-Origin Data Fetching

When iframe and parent page are on different origins, use request-iframe to securely fetch data:

/**
 * Inside iframe (different origin)
 * Note: allowedOrigins should be the real parent origin (example value below).
 */
const parentOrigin = 'https://parent.example.com';
const server = requestIframeServer({ secretKey: 'data-api', allowedOrigins: [parentOrigin] });

server.on('/api/data', async (req, res) => {
  // Fetch data from same-origin API (iframe can access same-origin resources)
  const data = await fetch('/api/internal/data').then(r => r.json());
  res.send(data);
});

/** Parent page (cross-origin) */
const targetOrigin = new URL(iframe.src).origin;
const client = requestIframeClient(iframe, { secretKey: 'data-api', targetOrigin, allowedOrigins: [targetOrigin] });
const response = await client.send('/api/data', {});
const data = response.data; // Successfully fetch cross-origin data

File Preview and Download

Process files in iframe, then transfer to parent page:

// Inside iframe: process file and return
server.on('/api/processFile', async (req, res) => {
  const { fileId } = req.body;
  const processedFile = await processFile(fileId);
  
  // Return processed file
  await res.sendFile(processedFile, {
    mimeType: 'application/pdf',
    fileName: `processed-${fileId}.pdf`
  });
});

// Parent page: download file
const response = await client.send('/api/processFile', { fileId: '123' });
if (response.data instanceof File || response.data instanceof Blob) {
  downloadFile(response.data);
}

How It Works

Communication Protocol

request-iframe implements an HTTP-like communication protocol on top of postMessage:

  Client (Parent Page)                      Server (iframe)
       │                                          │
       │  ──── REQUEST ────────────────────────>  │  Send request
       │                                          │
       │  <──── ACK (optional) ────────────────  │  Acknowledge receipt (controlled by request `requireAck`, default true)
       │                                          │
       │                                          │  Execute handler
       │                                          │
       │  <──── ASYNC (optional) ───────────────  │  If handler returns Promise
       │                                          │
       │  <──── RESPONSE ──────────────────────  │  Return result
       │                                          │
       │  ──── ACK (optional) ─────────────────>  │  Acknowledge receipt of response/error (ACK-only requireAck)
       │                                          │

Message Types

| Type | Direction | Description | |------|-----------|-------------| | request | Client → Server | Client initiates request | | ack | Receiver → Sender | Acknowledgment when requireAck: true and the message is accepted/handled (ACK-only) | | async | Server → Client | Notifies client this is an async task (sent when handler returns Promise) | | response | Server → Client | Returns response data | | error | Server → Client | Returns error information | | ping | Client → Server | Connection detection (isConnect() method, may use requireAck to confirm delivery) | | pong | Server → Client | Connection detection response | | stream_pull | Receiver → Sender | Stream pull: receiver requests next chunks (pull/ack protocol) |

Timeout Mechanism

request-iframe uses a three-stage timeout strategy to intelligently adapt to different scenarios:

client.send('/api/getData', data, {
  ackTimeout: 1000,       // Stage 1: ACK timeout (default 1000ms)
  timeout: 5000,          // Stage 2: Request timeout (default 5s)
  asyncTimeout: 120000,   // Stage 3: Async request timeout (default 120s)
  requireAck: true        // Whether to wait for server ACK before switching to stage 2 (default true)
});

Timeout Flow:

Send REQUEST
    │
    ▼
┌───────────────────┐    timeout    ┌─────────────────────────────┐
│ ackTimeout        │ ────────────> │ Error: ACK_TIMEOUT          │
│ (wait for ACK)    │                │ "Connection failed, Server  │
└───────────────────┘                │  not responding"           │
    │                                 └─────────────────────────────┘
    │ ACK received
    ▼
┌───────────────────┐    timeout    ┌─────────────────────────────┐
│ timeout           │ ────────────> │ Error: TIMEOUT               │
│ (wait for RESPONSE)│                │ "Request timeout"            │
└───────────────────┘                └─────────────────────────────┘
    │
    │ ASYNC received (optional)
    ▼
┌───────────────────┐    timeout    ┌─────────────────────────────┐
│ asyncTimeout      │ ────────────> │ Error: ASYNC_TIMEOUT         │
│ (wait for RESPONSE)│                │ "Async request timeout"      │
└───────────────────┘                └─────────────────────────────┘
    │
    │ RESPONSE received
    ▼
  Request Complete ✓

Why This Design?

| Stage | Timeout | Scenario | |-------|---------|----------| | ackTimeout | Short (1000ms) | Quickly detect if Server is online, avoid long waits for unreachable iframes. Increased from 500ms to accommodate slower environments or busy browsers | | timeout | Medium (5s) | Suitable for simple synchronous processing, like reading data, parameter validation | | asyncTimeout | Long (120s) | Suitable for complex async operations, like file processing, batch operations, third-party API calls |

Notes:

  • If you set requireAck: false, the request will skip the ACK stage and start timeout immediately.
  • Stream transfer has its own optional idle timeout: use streamTimeout (see Streaming).

Protocol Version

Each message contains a __requestIframe__ field identifying the protocol version, and a timestamp field recording message creation time:

{
  __requestIframe__: 2,  // Protocol version number
  timestamp: 1704067200000,  // Message creation timestamp (milliseconds)
  type: 'request',
  requestId: 'req_xxx',
  path: '/api/getData',
  body: { ... }
}

This enables:

  • Different library versions can handle compatibility
  • Current protocol version is 2. For the new stream pull/ack flow, both sides should use the same version.
  • Clear error messages when version is too low
  • timestamp facilitates debugging message delays and analyzing communication performance

Detailed Features

Interceptors

Request Interceptors

// Add request interceptor (unified token addition)
client.interceptors.request.use((config) => {
  config.headers = {
    ...config.headers,
    'Authorization': `Bearer ${getToken()}`
  };
  return config;
});

// Error handling
client.interceptors.request.use(
  (config) => config,
  (error) => {
    console.error('Request config error:', error);
    return Promise.reject(error);
  }
);

Response Interceptors

// Add response interceptor (unified data transformation)
client.interceptors.response.use((response) => {
  // Assume backend returns { code: 0, data: {...} } format
  if (response.data.code === 0) {
    response.data = response.data.data;
  }
  return response;
});

// Error handling
client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.code === 'TIMEOUT') {
      message.error('Request timeout, please retry');
    }
    return Promise.reject(error);
  }
);

Middleware

Server side supports Express-style middleware:

Global Middleware

// Logging middleware
server.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.path}`, req.body);
  next();
});

// Authentication middleware
server.use((req, res, next) => {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).send({ error: 'Unauthorized' });
  }
  // Verify token...
  next();
});

Path-Matching Middleware

// Only applies to /api/* paths
server.use('/api/*', (req, res, next) => {
  console.log('API request:', req.path);
  next();
});

// Regex matching
server.use(/^\/admin\//, (req, res, next) => {
  // Special handling for admin interfaces
  next();
});

// Array matching
server.use(['/user', '/profile'], (req, res, next) => {
  // User-related interfaces
  next();
});

Headers and Cookies

Note: The headers and cookies here are not real browser HTTP Headers and Cookies, but a message metadata passing mechanism simulated by request-iframe in HTTP style. Data is passed between iframes via postMessage and does not affect the browser's real Cookie storage.

Why This Design?

| Design Purpose | Description | |----------------|-------------| | API Style Consistency | Consistent usage with HTTP requests (axios/fetch) and server-side (Express) | | Lower Learning Curve | Developers familiar with HTTP can get started quickly without learning new APIs | | Third-Party Library Compatibility | Easy to reuse or adapt Express middleware, authentication libraries, etc., with minimal changes | | Cross-iframe State Sharing | Implement login state passing, permission validation between different iframes, solving state synchronization issues caused by iframe isolation | | Flexible Data Passing | Provides additional metadata channels beyond body, facilitating layered processing (e.g., middleware reads headers, business logic reads body) |

Automatic Cookie Management

request-iframe simulates HTTP's automatic cookie management mechanism:

Cookie Lifetime (Important):

  • In-memory only: Cookies are stored in the Client instance's internal CookieStore (not the browser's real cookies).
  • Lifecycle: By default, cookies live from requestIframeClient() creation until client.destroy().
  • open() / close(): These only enable/disable message handling; they do not clear the internal cookies.
  • Expiration: Expires / Max-Age are respected. Expired cookies are automatically filtered out when reading/sending (and can be removed via client.clearCookies() / client.removeCookie()).

How It Works (Similar to HTTP Set-Cookie):

  1. When Server sets cookie: Generate Set-Cookie string via res.cookie(name, value, options)
  2. When response returns: All Set-Cookie stored in headers['Set-Cookie'] array
  3. After Client receives response: Parse Set-Cookie header, save to Cookie storage based on Path and other attributes
  4. When Client sends request: Only carry path-matched cookies (similar to browser behavior)
// Server side: Set token on login (supports full Cookie options)
server.on('/api/login', (req, res) => {
  const { username, password } = req.body;
  // Verify user...
  
  // Set cookie (supports path, expires, maxAge, httpOnly, etc.)
  res.cookie('authToken', 'jwt_xxx', { path: '/api', httpOnly: true });
  res.cookie('userId', '12345', { path: '/' });
  res.send({ success: true });
});

// Server side: Read token in subsequent interfaces (client automatically carries path-matched cookies)
server.on('/api/getUserInfo', (req, res) => {
  const token = req.cookies['authToken'];  // Path matched, automatically carried
  const userId = req.cookies['userId'];     // Root path cookie, carried in all requests
  // Verify token...
  res.send({ name: 'Tom', age: 18 });
});

// Server side: Clear cookie
server.on('/api/logout', (req, res) => {
  res.clearCookie('authToken', { path: '/api' });
  res.send({ success: true });
});
// Client side: Login
await client.send('/api/login', { username: 'tom', password: '123' });

// Client side: Subsequent request to /api/getUserInfo (automatically carries authToken and userId)
const userInfoResponse = await client.send('/api/getUserInfo', {});
const userInfo = userInfoResponse.data;

// Client side: Request root path (only carries userId, because authToken's path is /api)
const rootResponse = await client.send('/other', {});
const rootData = rootResponse.data;

Client Cookie Management API

Client provides manual cookie management APIs with path isolation support:

// Get all cookies
client.getCookies();  // { authToken: 'jwt_xxx', userId: '12345' }

// Get cookies matching specified path
client.getCookies('/api');  // Only returns cookies matching /api

// Get specified cookie
client.getCookie('authToken');  // 'jwt_xxx'
client.getCookie('authToken', '/api');  // Get with specified path

// Manually set cookie (supports path options)
client.setCookie('theme', 'dark');  // Default path '/'
client.setCookie('apiConfig', 'v2', { path: '/api' });  // Specify path
client.setCookie('temp', 'xxx', { maxAge: 3600 });  // Expires in 1 hour

// Delete specified cookie
client.removeCookie('theme');  // Delete theme with path '/'
client.removeCookie('apiConfig', '/api');  // Delete cookie with specified path

// Clear all cookies (e.g., on logout)
client.clearCookies();

Headers Usage Example

// Client side: Send custom headers
const response = await client.send('/api/data', {}, {
  headers: {
    'X-Device-Id': 'device-123',
    'X-Platform': 'web',
    'Authorization': 'Bearer xxx'  // Can also pass token via headers
  }
});

// Server side: Read and set headers
server.on('/api/data', (req, res) => {
  // Read request headers
  const deviceId = req.headers['x-device-id'];
  const platform = req.headers['x-platform'];
  
  // Set response headers
  res.setHeader('X-Request-Id', req.requestId);
  res.set('X-Custom-Header', 'value');  // Chainable
  
  res.send({ data: 'ok' });
});

File Transfer

Note: File transfer (both Client→Server and Server→Client) is carried by the stream protocol under the hood. You normally only need to use client.sendFile() / res.sendFile().

Server → Client (Server sends file to client)

// Server side: Send file
server.on('/api/download', async (req, res) => {
  // String content
  await res.sendFile('Hello, World!', {
    mimeType: 'text/plain',
    fileName: 'hello.txt'
  });
  
  // Or Blob/File object
  const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
  await res.sendFile(blob, { fileName: 'data.bin' });
});

// Client side: Receive
const response = await client.send('/api/download', {});
if (response.data instanceof File || response.data instanceof Blob) {
  const file = response.data instanceof File ? response.data : null;
  const fileName = file?.name || 'download';
  
  // Download file directly using File/Blob
  const url = URL.createObjectURL(response.data);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName;
  a.click();
  URL.revokeObjectURL(url);
}

Client → Server (Client sends file to server)

Client sends file using sendFile() (or send(path, file)); server receives either req.body as File/Blob when autoResolve: true (default), or req.stream as IframeFileReadableStream when autoResolve: false.

// Client side: Send file (stream, autoResolve defaults to true)
const file = new File(['Hello Upload'], 'upload.txt', { type: 'text/plain' });
const response = await client.send('/api/upload', file);

// Or use sendFile explicitly
const blob = new Blob(['binary data'], { type: 'application/octet-stream' });
const response2 = await client.sendFile('/api/upload', blob, {
  fileName: 'data.bin',
  mimeType: 'application/octet-stream',
  autoResolve: true  // optional, default true: server gets File/Blob in req.body
});

// Server side: Receive file (autoResolve true → req.body is File/Blob)
server.on('/api/upload', async (req, res) => {
  const blob = req.body as Blob;  // or File when client sent File
  const text = await blob.text();
  console.log('Received file content:', text);
  res.send({ success: true, size: blob.size });
});

Note: When using client.send() with a File or Blob, it automatically dispatches to client.sendFile(). Server gets req.body as File/Blob when autoResolve is true (default), or req.stream / req.body as IframeFileReadableStream when autoResolve is false.

Streaming

Streaming is not only for large/chunked transfers, but also works well for long-lived subscription-style interactions (similar to SSE/WebSocket, but built on top of postMessage).

Long-lived subscription (push mode)

Notes:

  • IframeWritableStream defaults expireTimeout to asyncTimeout to avoid leaking long-lived streams. For real subscriptions, set a larger expireTimeout, or set expireTimeout: 0 to disable auto-expire (use with care and pair with cancel/reconnect).
  • res.sendStream(stream) waits until the stream ends. If you want to keep pushing via write(), do not await it; use void res.sendStream(stream) or keep the returned Promise.
  • If maxConcurrentRequestsPerClient is enabled, a long-lived stream occupies one in-flight request slot.
  • Event subscription: streams support stream.on(event, listener) (returns an unsubscribe function) for observability (e.g. start/data/read/write/cancel/end/error/timeout/expired). For consuming data, prefer for await.
/**
 * Server side: subscribe (long-lived)
 * - mode: 'push': writer calls write()
 * - expireTimeout: 0: disable auto-expire (use with care)
 */
server.on('/api/subscribe', (req, res) => {
  const stream = new IframeWritableStream({
    type: 'data',
    chunked: true,
    mode: 'push',
    expireTimeout: 0,
    /** optional: writer-side idle timeout while waiting for pull/ack */
    streamTimeout: 15000
  });

  /** do not await, otherwise it blocks until stream ends */
  void res.sendStream(stream);

  const timer = setInterval(() => {
    try {
      stream.write({ type: 'tick', ts: Date.now() });
    } catch {
      clearInterval(timer);
    }
  }, 1000);
});

/**
 * Client side: consume continuously (prefer for-await for long-lived streams)
 */
const resp = await client.send('/api/subscribe', {});
if (isIframeReadableStream(resp.stream)) {
  /** Optional: observe events */
  const off = resp.stream.on(StreamEvent.ERROR, ({ error }) => {
    console.error('stream error:', error);
  });

  for await (const evt of resp.stream) {
    console.log('event:', evt);
  }

  off();
}

Server → Client (Server sends stream to client)

import {
  StreamEvent,
  IframeWritableStream, 
  IframeFileWritableStream,
  isIframeReadableStream,
  isIframeFileReadableStream 
} from 'request-iframe';

/**
 * How to choose (data stream vs file/byte stream)
 *
 * - IframeWritableStream / IframeReadableStream:
 *   For application-level data (objects/strings), relies on structured clone.
 * - IframeFileWritableStream / IframeFileReadableStream:
 *   For byte sequences (files/binary/UTF-8 text files). Chunks represent bytes.
 *   - Text file recommended: IframeFileWritableStream.fromText(...) + fileStream.readAsText()
 *   - Binary recommended: yield Uint8Array/ArrayBuffer (transferables when possible)
 */

// Server side: Send data stream using iterator
server.on('/api/stream', async (req, res) => {
  const stream = new IframeWritableStream({
    type: 'data',
    chunked: true,
    mode: 'pull', // default: pull/ack protocol (backpressure)
    // Optional: auto-expire stream to avoid leaking resources (default: asyncTimeout)
    // expireTimeout: 120000,
    // Optional: writer-side idle timeout while waiting for pull/ack
    // streamTimeout: 10000,
    // Generate data using async iterator
    iterator: async function* () {
      for (let i = 0; i < 10; i++) {
        yield { chunk: i, data: `Data chunk ${i}` };
        await new Promise(r => setTimeout(r, 100)); // Simulate delay
      }
    }
  });
  
  await res.sendStream(stream);
});

// Server side: Option 2 (recommended) - create a file stream from Blob/File via from()
server.on('/api/fileStream2', async (req, res) => {
  const blob = new Blob([/* file bytes */], { type: 'application/octet-stream' });
  const stream = await IframeFileWritableStream.from({
    content: blob,
    fileName: 'large-file.bin',
    mimeType: 'application/octet-stream',
    chunked: true,
    chunkSize: 256 * 1024
  });
  await res.sendStream(stream);
});

// Client side: Receive stream data
const response = await client.send('/api/stream', {}, { streamTimeout: 10000 });

// Check if it's a stream response
if (isIframeReadableStream(response.stream)) {
  // Sender stream mode (from stream_start)
  console.log('Stream mode:', response.stream.mode); // 'pull' | 'push' | undefined

  // Method 1: Read all data at once
  const allData = await response.stream.read();
  // If you want a consistent return type (always an array of chunks), use readAll()
  const allChunks = await response.stream.readAll();
  
  // Method 2: Read chunk by chunk using async iterator (consume defaults to true)
  for await (const chunk of response.stream) {
    console.log('Received chunk:', chunk);
  }
  
  // Listen to stream end
  response.stream.onEnd(() => {
    console.log('Stream ended');
  });
  
  // Listen to stream error
  response.stream.onError((error) => {
    console.error('Stream error:', error);
  });
  
  // Cancel stream
  response.stream.cancel('User cancelled');
}

Text file convenience

// Server side: send a UTF-8 text file
server.on('/api/textFile', async (req, res) => {
  const stream = await IframeFileWritableStream.fromText({
    text: 'hello',
    fileName: 'hello.txt',
    chunked: true,
    chunkSize: 64 * 1024
  });
  await res.sendStream(stream);
});

// Client side: read as UTF-8 text
const resp = await client.send('/api/textFile', {});
if (isIframeFileReadableStream(resp.stream)) {
  const text = await resp.stream.readAsText();
  console.log(text);
}

Client → Server (Client sends stream to server)

import { IframeWritableStream } from 'request-iframe';

// Client side: Send stream to server
const stream = new IframeWritableStream({
  chunked: true,
  iterator: async function* () {
    for (let i = 0; i < 5; i++) {
      yield `Chunk ${i}`;
      await new Promise(r => setTimeout(r, 50));
    }
  }
});

// Use sendStream to send stream as request body
const response = await client.sendStream('/api/uploadStream', stream);
console.log('Upload result:', response.data);

// Or use send() - it automatically dispatches to sendStream for IframeWritableStream
const stream2 = new IframeWritableStream({
  next: async () => ({ data: 'single chunk', done: true })
});
const response2 = await client.send('/api/uploadStream', stream2);

// Server side: Receive stream
server.on('/api/uploadStream', async (req, res) => {
  // req.stream is available when client sends stream
  if (req.stream) {
    const chunks: string[] = [];
    
    // Read stream chunk by chunk
    for await (const chunk of req.stream) {
      chunks.push(chunk);
      console.log('Received chunk:', chunk);
    }
    
    res.send({ 
      success: true, 
      chunkCount: chunks.length,
      chunks 
    });
  } else {
    res.status(400).send({ error: 'Expected stream body' });
  }
});

Stream Types:

| Type | Description | |------|-------------| | IframeWritableStream | Writer/producer stream: created by whichever side is sending the stream (server→client response stream, or client→server request stream) | | IframeFileWritableStream | File writer/producer stream (supports Uint8Array/ArrayBuffer chunks; uses transferables when possible) | | IframeReadableStream | Reader/consumer stream for receiving regular data (regardless of which side sent it) | | IframeFileReadableStream | File reader/consumer stream (supports binary chunks) |

Note: File stream chunks represent bytes. This version transfers file chunks as binary (ArrayBuffer/Uint8Array) and will use transferables when possible to reduce copying. If you manually yield a string chunk, it will be encoded into bytes using UTF-8; for binary data, yield Uint8Array/ArrayBuffer directly. For large files, prefer chunked file streams (chunked: true) and keep chunk sizes moderate (e.g. 256KB–1MB).

Stream timeouts:

  • options.streamTimeout (request option): client-side stream idle timeout while consuming response.stream (data/file streams). When triggered, the client performs a heartbeat check (by default client.isConnect()); if not alive, the stream fails as disconnected.
  • expireTimeout (writable stream option): writer-side stream lifetime. When expired, the writer sends stream_error and the reader will fail the stream with STREAM_EXPIRED.
  • streamTimeout (writable stream option): writer-side idle timeout. If the writer does not receive stream_pull for a long time, it will heartbeat-check and fail to avoid wasting resources.
  • maxPendingChunks (writable stream option): max number of pending (unsent) chunks kept in memory on the writer side (optional). Important for long-lived push streams: if the receiver stops pulling, continued write() calls will accumulate in the writer queue.
  • maxPendingBytes (writable stream option): max bytes of pending (unsent) chunks kept in memory on the writer side (optional). Useful when a single write() may enqueue a very large chunk.

Pull/Ack protocol (default):

  • Writer only sends stream_data when it has received stream_pull, enabling real backpressure.
    • Disconnect detection does not rely on a dedicated per-frame ack message type, but uses streamTimeout + heartbeat(isConnect).

consume default change:

  • for await (const chunk of response.stream) defaults to consume and drop already iterated chunks (consume: true) to prevent unbounded memory growth for long streams.

Connection Detection

// Detect if Server is reachable
const isConnected = await client.isConnect();
if (isConnected) {
  console.log('Connection OK');
} else {
  console.log('Connection failed');
}

Response Acknowledgment

Server can require Client to acknowledge receipt of response:

server.on('/api/important', async (req, res) => {
  // requireAck: true means client needs to acknowledge
  const acked = await res.send(data, { requireAck: true });
  
  if (acked) {
    console.log('Client acknowledged receipt');
  } else {
    console.log('Client did not acknowledge (timeout)');
  }
});

Note: Client acknowledgment (ack) is sent automatically by the library when the response/error is accepted by the client (i.e., there is a matching pending request). You don't need to manually send ack.

Trace Mode

By default, request-iframe only prints warn/error logs (to avoid noisy console in production).

Enable trace mode (or set a log level) to view detailed communication logs:

import { LogLevel } from 'request-iframe';

const client = requestIframeClient(iframe, { 
  secretKey: 'demo',
  trace: true // equivalent to LogLevel.TRACE
});

const server = requestIframeServer({ 
  secretKey: 'demo',
  trace: LogLevel.INFO // enable info/warn/error logs (less verbose than trace)
});

// Console output:
// [request-iframe] [INFO] 📤 Request Start { path: '/api/getData', ... }
// [request-iframe] [INFO] 📨 ACK Received { requestId: '...' }
// [request-iframe] [INFO] ✅ Request Success { status: 200, data: {...} }

trace supports:

  • true / false
  • 'trace' | 'info' | 'warn' | 'error' | 'silent' (or LogLevel.*)

Notes:

  • When trace is LogLevel.TRACE or LogLevel.INFO, the library will also attach built-in debug interceptors/listeners for richer request/response logs.
  • When trace is LogLevel.WARN / LogLevel.ERROR / LogLevel.SILENT, it only affects log output level (no extra debug interceptors attached).

Internationalization

import { setMessages } from 'request-iframe';

// Switch to Chinese
setMessages({
  ACK_TIMEOUT: 'ACK acknowledgment timeout, waited {0} milliseconds',
  REQUEST_TIMEOUT: 'Request timeout, waited {0} milliseconds',
  REQUEST_FAILED: 'Request failed',
  METHOD_NOT_FOUND: 'Method not found',
  MIDDLEWARE_ERROR: 'Middleware error',
  IFRAME_NOT_READY: 'iframe not ready'
});

API Reference

requestIframeClient(target, options?)

Create a Client instance.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | target | HTMLIFrameElement \| Window | Target iframe element or window object | | options.secretKey | string | Message isolation identifier (optional) | | options.trace | boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent' | Trace/log level (optional). Default logs are warn/error only | | options.strict | boolean | Strict mode (recommended for same-origin defaults). If you did not explicitly configure targetOrigin/allowedOrigins/validateOrigin, defaults are constrained to window.location.origin (same-origin only). Note: strict is NOT a cross-origin security configuration; for cross-origin you must explicitly set targetOrigin + allowedOrigins/validateOrigin. | | options.targetOrigin | string | Override postMessage targetOrigin for sending (optional). If target is a Window, default is *. | | options.ackTimeout | number | Global default ACK acknowledgment timeout (ms), default 1000 | | options.timeout | number | Global default request timeout (ms), default 5000 | | options.asyncTimeout | number | Global default async timeout (ms), default 120000 | | options.requireAck | boolean | Global default for request delivery ACK (default true). If false, requests skip the ACK stage and start timeout immediately | | options.streamTimeout | number | Global default stream idle timeout (ms) when consuming response.stream (optional) | | options.allowedOrigins | string \| RegExp \| Array<string \| RegExp> | Allowlist for incoming message origins (optional, recommended for production) | | options.validateOrigin | (origin, data, context) => boolean | Custom origin validator (optional, higher priority than allowedOrigins) |

Returns: RequestIframeClient

Notes about target: Window:

  • You must have a Window reference (e.g. from window.open(), window.opener, or MessageEvent.source).
  • You cannot communicate with an arbitrary browser tab by URL.
  • For security, prefer setting a strict targetOrigin (avoid *) and configure allowedOrigins / validateOrigin.
  • strict: true only constrains defaults to the current origin (same-origin only); it does not automatically make a cross-origin setup secure.

Production configuration template:

import { requestIframeClient, requestIframeServer } from 'request-iframe';

/**
 * Recommended: explicitly constrain 3 things
 * - secretKey: isolate different apps/instances (avoid cross-talk)
 * - targetOrigin: postMessage targetOrigin (strongly avoid '*' for Window targets)
 * - allowedOrigins / validateOrigin: incoming origin allowlist validation
 */
const secretKey = 'my-app';
const targetOrigin = 'https://child.example.com';
const allowedOrigins = [targetOrigin];

// Client (parent)
const client = requestIframeClient(window.open(targetOrigin)!, {
  secretKey,
  targetOrigin,
  allowedOrigins
});

// Server (child/iframe)
const server = requestIframeServer({
  secretKey,
  allowedOrigins,
  // Mitigate message explosion (tune as needed)
  maxConcurrentRequestsPerClient: 50
});

Production configuration template (iframe target):

import { requestIframeClient, requestIframeServer } from 'request-iframe';

/**
 * For iframe targets, you can derive targetOrigin from iframe.src, and use it as the allowedOrigins allowlist.
 */
const iframe = document.querySelector('iframe')!;
const targetOrigin = new URL(iframe.src).origin;
const secretKey = 'my-app';

// Client (parent)
const client = requestIframeClient(iframe, {
  secretKey,
  // Explicitly set it (even though the library can derive it) to avoid accidentally using '*'
  targetOrigin,
  allowedOrigins: [targetOrigin]
});

// Server (inside iframe)
const server = requestIframeServer({
  secretKey,
  allowedOrigins: [targetOrigin],
  maxConcurrentRequestsPerClient: 50
});

requestIframeServer(options?)

Create a Server instance.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | options.secretKey | string | Message isolation identifier (optional) | | options.trace | boolean \| 'trace' \| 'info' \| 'warn' \| 'error' \| 'silent' | Trace/log level (optional). Default logs are warn/error only | | options.strict | boolean | Strict mode (recommended for same-origin defaults). If you did not explicitly configure allowedOrigins/validateOrigin, it defaults to allowedOrigins: [window.location.origin] (same-origin only). Note: strict is NOT a cross-origin security configuration; for cross-origin you must explicitly configure allowlists/validators. | | options.ackTimeout | number | Wait for client acknowledgment timeout (ms), default 1000 | | options.maxConcurrentRequestsPerClient | number | Max concurrent in-flight requests per client (per origin + creatorId). Default Infinity | | options.allowedOrigins | string \| RegExp \| Array<string \| RegExp> | Allowlist for incoming message origins (optional, recommended for production) | | options.validateOrigin | (origin, data, context) => boolean | Custom origin validator (optional, higher priority than allowedOrigins) |

Returns: RequestIframeServer

requestIframeEndpoint(target, options?)

Create an endpoint facade (client + server) for a peer window/iframe.

It can:

  • send requests to the peer: endpoint.send(...)
  • handle requests from the peer: endpoint.on(...), endpoint.use(...), endpoint.map(...)

Notes:

  • Client and server instances are lazily created (only when you first access send/handlers APIs).
  • options.id (if provided) becomes the shared ID for both sides (client + server); otherwise a random one is generated.
  • options.trace follows the same leveled logging rules as client/server (LogLevel.* recommended).

Example (bidirectional via endpoint, recommended):

import { requestIframeEndpoint, LogLevel } from 'request-iframe';

// Parent page (has iframe element)
const iframe = document.querySelector('iframe')!;
const parentEndpoint = requestIframeEndpoint(iframe, {
  secretKey: 'demo',
  trace: LogLevel.INFO
});
parentEndpoint.on('/notify', (req, res) => res.send({ ok: true, echo: req.body }));

// Iframe page (has window.parent)
const iframeEndpoint = requestIframeEndpoint(window.parent, {
  secretKey: 'demo',
  targetOrigin: 'https://parent.example.com',
  trace: true
});
iframeEndpoint.on('/api/ping', (req, res) => res.send({ ok: true }));

// Either side can now send + handle
await parentEndpoint.send('/api/ping', { from: 'parent' });
await iframeEndpoint.send('/notify', { from: 'iframe' });

Production configuration template (recommended):

import { requestIframeEndpoint, LogLevel } from 'request-iframe';

const secretKey = 'my-app';
const iframe = document.querySelector('iframe')!;
const targetOrigin = new URL(iframe.src).origin;

const endpoint = requestIframeEndpoint(iframe, {
  secretKey,
  targetOrigin,
  allowedOrigins: [targetOrigin],
  // Mitigate message explosion (tune as needed)
  maxConcurrentRequestsPerClient: 50,
  // Logs: default is warn/error; choose info for debugging
  trace: LogLevel.WARN
});

Client API

client.send(path, body?, options?)

Send a request. Automatically dispatches to sendFile() or sendStream() based on body type.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | path | string | Request path | | body | any | Request data (optional). Can be plain object, File, Blob, or IframeWritableStream. Automatically dispatches: File/Blob → sendFile(), IframeWritableStream → sendStream() | | options.ackTimeout | number | ACK acknowledgment timeout (ms), default 1000 | | options.timeout | number | Request timeout (ms), default 5000 | | options.asyncTimeout | number | Async timeout (ms), default 120000 | | options.requireAck | boolean | Whether to require server delivery ACK (default true). If false, skips ACK stage | | options.streamTimeout | number | Stream idle timeout (ms) while consuming response.stream (optional) | | options.headers | object | Request headers (optional) | | options.cookies | object | Request cookies (optional, merged with internally stored cookies, passed-in takes priority) | | options.requestId | string | Custom request ID (optional) |

Returns: Promise<Response>

interface Response<T = any> {
  data: T;                    // Response data (File/Blob for auto-resolved file streams)
  status: number;             // Status code
  statusText: string;         // Status text
  requestId: string;          // Request ID
  headers?: Record<string, string | string[]>;  // Response headers (Set-Cookie is array)
  stream?: IIframeReadableStream<T>;  // Stream response (if any)
}

Examples:

// Send plain object (auto Content-Type: application/json)
await client.send('/api/data', { name: 'test' });

// Send string (auto Content-Type: text/plain)
await client.send('/api/text', 'Hello');

// Send File/Blob (auto-dispatches to sendFile)
const file = new File(['content'], 'test.txt');
await client.send('/api/upload', file);

// Send stream (auto-dispatches to sendStream)
const stream = new IframeWritableStream({ iterator: async function* () { yield 'data'; } });
await client.send('/api/uploadStream', stream);

client.sendFile(path, content, options?)

Send file as request body (via stream; server receives File/Blob when autoResolve is true).

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | path | string | Request path | | content | string \| Blob \| File | File content to send | | options.mimeType | string | File MIME type (optional, uses content.type if available) | | options.fileName | string | File name (optional) | | options.autoResolve | boolean | If true (default), server receives File/Blob in req.body; if false, server gets req.stream / req.body as IframeFileReadableStream | | options.ackTimeout | number | ACK acknowledgment timeout (ms), default 1000 | | options.timeout | number | Request timeout (ms), default 5000 | | options.asyncTimeout | number | Async timeout (ms), default 120000 | | options.requireAck | boolean | Whether to require server delivery ACK (default true). If false, skips ACK stage | | options.streamTimeout | number | Stream idle timeout (ms) while consuming response.stream (optional) | | options.headers | object | Request headers (optional) | | options.cookies | object | Request cookies (optional) | | options.requestId | string | Custom request ID (optional) |

Returns: Promise<Response>

Note: The file is sent via stream. When autoResolve is true (default), the server receives req.body as File/Blob; when false, the server receives req.stream / req.body as IframeFileReadableStream.

client.sendStream(path, stream, options?)

Send stream as request body (server receives readable stream).

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | path | string | Request path | | stream | IframeWritableStream | Writable stream to send | | options.ackTimeout | number | ACK acknowledgment timeout (ms), default 1000 | | options.timeout | number | Request timeout (ms), default 5000 | | options.asyncTimeout | number | Async timeout (ms), default 120000 | | options.requireAck | boolean | Whether to require server delivery ACK (default true). If false, skips ACK stage | | options.streamTimeout | number | Stream idle timeout (ms) while consuming response.stream (optional) | | options.headers | object | Request headers (optional) | | options.cookies | object | Request cookies (optional) | | options.requestId | string | Custom request ID (optional) |

Returns: Promise<Response>

Note: On the server side, the stream is available as req.stream (an IIframeReadableStream). You can iterate over it using for await (const chunk of req.stream).

client.isConnect()

Detect if Server is reachable.

Returns: Promise<boolean>

client.interceptors

Interceptor manager.

// Request interceptor
client.interceptors.request.use(onFulfilled, onRejected?);

// Response interceptor
client.interceptors.response.use(onFulfilled, onRejected?);

Server API

server.on(path, handler)

Register route handler.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | path | string | Request path | | handler | ServerHandler | Handler function |

type ServerHandler = (req: ServerRequest, res: ServerResponse) => any | Promise<any>;

ServerRequest interface:

interface ServerRequest {
  body: any;                    // Request body (plain data, or File/Blob when client sendFile with autoResolve true)
  stream?: IIframeReadableStream; // Request stream (when client sends via sendStream or sendFile with autoResolve false)
  headers: Record<string, string>; // Request headers
  cookies: Record<string, string>;  // Request cookies
  path: string;                 // Request path
  params: Record<string, string>; // Path parameters extracted from route pattern (e.g., { id: '123' } for '/api/users/:id' and '/api/users/123')
  requestId: string;            // Request ID
  origin: string;               // Sender origin
  source: Window;                // Sender window
  res: ServerResponse;          // Response object
}

Note:

  • When client sends a file via sendFile() (or send(path, file)), the file is sent via stream. If autoResolve is true (default), req.body is the resolved File/Blob; if false, req.stream / req.body is an IIframeReadableStream (e.g. IframeFileReadableStream).
  • When client sends a stream via sendStream(), req.stream is available as an IIframeReadableStream. You can iterate over it using for await (const chunk of req.stream).
  • Path parameters: You can use Express-style route parameters (e.g., /api/users/:id) to extract path segments. The extracted parameters are available in req.params. For example, registering /api/users/:id and receiving /api/users/123 will set req.params.id to '123'.

Path Parameters Example:

// Register route with parameter
server.on('/api/users/:id', (req, res) => {
  const userId = req.params.id; // '123' when path is '/api/users/123'
  res.send({ userId });
});

// Multiple parameters
server.on('/api/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.send({ userId, postId });
});

Handler return value behavior

  • If your handler does not call res.send() / res.json() / res.sendFile() / res.sendStream(), but it returns a value that is not undefined, then the server will treat it as a successful result and automatically send it back to the client (equivalent to res.send(returnValue)).
  • For async handlers (Promise): if the promise resolves to a value that is not undefined and no response has been sent yet, it will also be auto-sent.
  • If the handler (or resolved promise) returns undefined and no response method was called, the server will respond with error code NO_RESPONSE.

Examples:

// Sync: auto-send return value
server.on('/api/hello', () => {
  return { message: 'hello' };
});

// Async: auto-send resolved value
server.on('/api/user', async (req) => {
  const user = await getUser(req.body.userId);
  return user; // auto-send if not undefined
});

// If you manually send, return value is ignored
server.on('/api/manual', (req, res) => {
  res.send({ ok: true });
  return { ignored: true };
});

server.off(path)

Remove route handler.

server.map(handlers)

Batch register handlers.

server.map({
  '/api/users': (req, res) => res.send([...]),
  '/api/posts': (req, res) => res.send([...])
});

server.use(middleware)

server.use(path, middleware)

Register middleware.

// Global middleware
server.use((req, res, next) => { ... });

// Path-matching middleware
server.use('/api/*', (req, res, next) => { ... });
server.use(/^\/admin/, (req, res, next) => { ... });
server.use(['/a', '/b'], (req, res, next) => { ... });

server.destroy()

Destroy Server instance, remove all listeners.


React Hooks

request-iframe provides React hooks for easy integration in React applications. Import hooks from request-iframe/react:

Note: React is only required if you use request-iframe/react. Installing request-iframe alone does not require React.

import { useClient, useServer, useServerHandler, useServerHandlerMap } from 'request-iframe/react';

useClient(targetFnOrRef, options?, deps?)

React hook for using request-iframe client.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | targetFnOrRef | (() => HTMLIFrameElement \| Window \| null) \| RefObject<HTMLIFrameElement \| Window> | Function that returns iframe element or Window object, or a React ref object | | options | RequestIframeClientOptions | Client options (optional) | | deps | readonly unknown[] | Dependency array (optional, for re-creating client when dependencies change) |

Returns: RequestIframeClient | null

Example:

import { useClient } from 'request-iframe/react';
import { useRef } from 'react';

const MyComponent = () => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const client = useClient(iframeRef, { secretKey: 'my-app' });

  const handleClick = async () => {
    if (client) {
      const response = await client.send('/api/data', { id: 1 });
      console.log(response.data);
    }
  };

  return (
    <div>
      <iframe ref={iframeRef} src="/iframe.html" />
      <button onClick={handleClick}>Send Request</button>
    </div>
  );
};

Using function instead of ref:

const MyComponent = () => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const client = useClient(() => iframeRef.current, { secretKey: 'my-app' });
  // ...
};

useServer(options?, deps?)

React hook for using request-iframe server.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | options | RequestIframeServerOptions | Server options (optional) | | deps | readonly unknown[] | Dependency array (optional, for re-creating server when dependencies change) |

Returns: RequestIframeServer | null

Example:

import { useServer } from 'request-iframe/react';

const MyComponent = () => {
  const server = useServer({ secretKey: 'my-app' });

  useEffect(() => {
    if (!server) return;

    const off = server.on('/api/data', (req, res) => {
      res.send({ data: 'Hello' });
    });

    return off; // Cleanup on unmount
  }, [server]);

  return <div>Server Component</div>;
};

useServerHandler(server, path, handler, deps?)

React hook for registering a single server handler with automatic cleanup and closure handling.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | server | RequestIframeServer \| null | Server instance (from useServer) | | path | string | Route path | | handler | ServerHandler | Handler function | | deps | readonly unknown[] | Dependency array (optional, for re-registering when dependencies change) |

Example:

import { useServer, useServerHandler } from 'request-iframe/react';
import { useState } from 'react';

const MyComponent = () => {
  const server = useServer();
  const [userId, setUserId] = useState(1);

  // Handler automatically uses latest userId value
  useServerHandler(server, '/api/user', (req, res) => {
    res.send({ userId, data: 'Hello' });
  }, [userId]); // Re-register when userId changes

  return <div>Server Component</div>;
};

Key Features:

  • Automatically handles closure issues - always uses latest values from dependencies
  • Automatically unregisters handler on unmount or when dependencies change
  • No need to manually manage handler registration/cleanup

useServerHandlerMap(server, map, deps?)

React hook for registering multiple server handlers at once with automatic cleanup.

Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | server | RequestIframeServer \| null | Server instance (from useServer) | | map | Record<string, ServerHandler> | Map of route paths and handler functions | | deps | readonly unknown[] | Dependency array (optional, for re-registering when dependencies change) |

Example:

import { useServer, useServerHandlerMap } from 'request-iframe/react';
import { useState } from 'react';

const MyComponent = () => {
  const server = useServer();
  const [userId, setUserId] = useState(1);

  // Register multiple handlers at once
  useServerHandlerMap(server, {
    '/api/user': (req, res) => {
      res.send({ userId, data: 'User data' });
    },
    '/api/posts': (req, res) => {
      res.send({ userId, data: 'Posts data' });
    }
  }, [userId]); // Re-register all handlers when userId changes

  return <div>Server Component</div>;
};

Key Features:

  • Batch registration of multiple handlers
  • Automatically handles closure issues - always uses latest values from dependencies
  • Automatically unregisters all handlers on unmount or when dependencies change
  • Efficient - only re-registers when map keys change

Complete Example

Here's a complete example showing how to use React hooks in a real application:

import { useClient, useServer, useServerHandler } from 'request-iframe/react';
import { useRef, useState } from 'react';

// Parent Component (Client)
const ParentComponent = () => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const client = useClient(iframeRef, { secretKey: 'my-app' });
  const [data, setData] = useState(null);

  const fetchData = async () => {
    if (!client) return;
    
    try {
      const response = await client.send('/api/data', { id: 1 });
      setData(response.data);
    } catch (error) {
      console.error('Request failed:', error);
    }
  };

  return (
    <div>
      <iframe ref={iframeRef} src="/iframe.html" />
      <button onClick={fetchData}>Fetch Data</button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
};

// Iframe Component (Server)
const IframeComponent = () => {
  const server = useServer({ secretKey: 'my-app' });
  const [userId, setUserId] = useState(1);

  // Register handler with automatic cleanup
  useServerHandler(server, '/api/data', async (req, res) => {
    // Handler always uses latest userId value
    const userData = await fetchUserData(userId);
    res.send(userData);
  }, [userId]);

  return (
    <div>
      <p>User ID: {userId}</p>
      <button onClick={() => setUserId(userId + 1)}>Increment</button>
    </div>
  );
};

Best Practices

  1. Always check for null: Client and server hooks may return null initially or when target is unavailable:

    const client = useClient(iframeRef);
    if (!client) return null; // Handle null case
  2. Use dependency arrays: Pass dependencies to hooks to ensure handlers use latest values:

    useServerHandler(server, '/api/data', (req, res) => {
      res.send({ userId }); // Always uses latest userId
    }, [userId]); // Re-register when userId changes
  3. Cleanup is automatic: Hooks automatically clean up on unmount, but you can also manually unregister:

    useEffect(() => {
      if (!server) return;
      const off = server.on('/api/data', handler);
      return off; // Manual cleanup (optional, hooks do this automatically)
    }, [server]);

Error Handling

Error Codes

| Error Code | Description | |------------|-------------| | ACK_TIMEOUT | ACK acknowledgment timeout (did not receive ACK) | | TIMEOUT | Synchronous request timeout | | ASYNC_TIMEOUT | Async request timeout | | REQUEST_ERROR | Request processing error | | METHOD_NOT_FOUND | Handler not found | | NO_RESPONSE | Handler did not send response | | PROTOCOL_UNSUPPORTED | Protocol version not supported | | IFRAME_NOT_READY | iframe not ready | | STREAM_ERROR | Stream transfer error | | STREAM_TIMEOUT | Stream idle timeout | | STREAM_EXPIRED | Stream expired (writable stream lifetime exceeded) | | STREAM_CANCELLED | Stream cancelled | | STREAM_NOT_BOUND | Stream not bound to request context | | STREAM_START_TIMEOUT | Stream start timeout (request body stream_start not received in time) | | TOO_MANY_REQUESTS | Too many concurrent requests (server-side limit) |

Error Handling Example

try {
  const response = await client.send('/api/getData', { id: 1 });
} catch (error) {
  switch (error.code) {
    case 'ACK_TIMEOUT':
      console.error('Cannot connect to iframe');
      break;
    case 'TIMEOUT':
      console.error('Request timeout');
      break;
    case 'METHOD_NOT_FOUND':
      console.error('Interface does not exist');
      break;
    default:
      console.error('Request failed:', error.message);
  }
}

FAQ

1. What is secretKey used for?

secretKey is used for message isolation. When there are multiple iframes or multiple request-iframe instances on a page, using different secretKey values can prevent message cross-talk:

/**
 * Communication for iframe A
 * - Parent page should allowlist iframe A origin
 * - Inside iframe A should allowlist parent origin
 */
const iframeASrc = iframeA.getAttribute('src');
if (!iframeASrc) throw new Error('iframeA src is empty');
const iframeAOrigin = new URL(iframeASrc, window.location.href).origin;
const clientA = requestIframeClient(iframeA, {
  secretKey: 'app-a',
  targetOrigin: iframeAOrigin,
  allowedOrigins: [iframeAOrigin]
});
const parentOrigin = 'https://parent.com';
const serverA = requestIframeServer({ secretKey: 'app-a', allowedOrigins: [parentOrigin] });

/**
 * Communication for iframe B
 * - Same pattern as iframe A
 */
const iframeBSrc = iframeB.getAttribute('src');
if (!iframeBSrc) throw new Error('iframeB src is empty');
const iframeBOrigin = new URL(iframeBSrc, window.location.href).origin;
const clientB = requestIframeClient(iframeB, {
  secretKey: 'app-b',
  targetOrigin: iframeBOrigin,
  allowedOrigins: [iframeBOrigin]
});
const serverB = requestIframeServer({ secretKey: 'app-b', allowedOrigins: [parentOrigin] });

2. Why is ACK acknowledgment needed?

ACK mechanism is similar to TCP handshake, used for:

  1. Quickly confirm if Server is online
  2. Distinguish between "connection failure" and "request timeout"
  3. Support timeout switching for async tasks

3. How to handle iframe cross-origin?

postMessage itself supports cross-origin communication, request-iframe handles it automatically:

/**
 * Parent page (https://parent.com)
 * - targetOrigin/allowedOrigins should be the iframe origin
 */
const ifr