request-iframe
v0.2.2
Published
Communicate with iframes like sending HTTP requests
Maintainers
Readme
request-iframe
Communicate with iframes/windows like sending HTTP requests! A cross-origin browser communication library based on postMessage.
📑 Table of Contents
- Why request-iframe?
- Features
- Installation
- Quick Start
- Use Cases
- How It Works
- Detailed Features
- API Reference
- React Hooks
- Error Handling
- FAQ
- Development
- License
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
tracelevel - 🌍 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-iframeRequirements: 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→ globalRequestIframe - React bundle output:
cdn/request-iframe-react.umd(.min).js→ globalRequestIframeReact(requiresReactglobal andRequestIframeglobal)
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 needsend()+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
targetOriginandallowedOrigins/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 dataFile 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 starttimeoutimmediately. - 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
timestampfacilitates 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
headersandcookieshere 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 viapostMessageand 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 untilclient.destroy(). open()/close(): These only enable/disable message handling; they do not clear the internal cookies.- Expiration:
Expires/Max-Ageare respected. Expired cookies are automatically filtered out when reading/sending (and can be removed viaclient.clearCookies()/client.removeCookie()).
How It Works (Similar to HTTP Set-Cookie):
- When Server sets cookie: Generate
Set-Cookiestring viares.cookie(name, value, options) - When response returns: All
Set-Cookiestored inheaders['Set-Cookie']array - After Client receives response: Parse
Set-Cookieheader, save to Cookie storage based on Path and other attributes - 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:
IframeWritableStreamdefaultsexpireTimeouttoasyncTimeoutto avoid leaking long-lived streams. For real subscriptions, set a largerexpireTimeout, or setexpireTimeout: 0to 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 viawrite(), do notawaitit; usevoid res.sendStream(stream)or keep the returned Promise.- If
maxConcurrentRequestsPerClientis 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, preferfor 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 astringchunk, it will be encoded into bytes using UTF-8; for binary data, yieldUint8Array/ArrayBufferdirectly. 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 consumingresponse.stream(data/file streams). When triggered, the client performs a heartbeat check (by defaultclient.isConnect()); if not alive, the stream fails as disconnected.expireTimeout(writable stream option): writer-side stream lifetime. When expired, the writer sendsstream_errorand the reader will fail the stream withSTREAM_EXPIRED.streamTimeout(writable stream option): writer-side idle timeout. If the writer does not receivestream_pullfor 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-livedpushstreams: if the receiver stops pulling, continuedwrite()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 singlewrite()may enqueue a very large chunk.
Pull/Ack protocol (default):
- Writer only sends
stream_datawhen it has receivedstream_pull, enabling real backpressure.- Disconnect detection does not rely on a dedicated per-frame ack message type, but uses
streamTimeout + heartbeat(isConnect).
- Disconnect detection does not rely on a dedicated per-frame ack message type, but uses
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 sendack.
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'(orLogLevel.*)
Notes:
- When
traceisLogLevel.TRACEorLogLevel.INFO, the library will also attach built-in debug interceptors/listeners for richer request/response logs. - When
traceisLogLevel.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
Windowreference (e.g. fromwindow.open(),window.opener, orMessageEvent.source). - You cannot communicate with an arbitrary browser tab by URL.
- For security, prefer setting a strict
targetOrigin(avoid*) and configureallowedOrigins/validateOrigin. strict: trueonly 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.tracefollows 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()(orsend(path, file)), the file is sent via stream. IfautoResolveis true (default),req.bodyis the resolved File/Blob; if false,req.stream/req.bodyis anIIframeReadableStream(e.g.IframeFileReadableStream). - When client sends a stream via
sendStream(),req.streamis available as anIIframeReadableStream. You can iterate over it usingfor 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 inreq.params. For example, registering/api/users/:idand receiving/api/users/123will setreq.params.idto'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 notundefined, then the server will treat it as a successful result and automatically send it back to the client (equivalent tores.send(returnValue)). - For async handlers (
Promise): if the promise resolves to a value that is notundefinedand no response has been sent yet, it will also be auto-sent. - If the handler (or resolved promise) returns
undefinedand no response method was called, the server will respond with error codeNO_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. Installingrequest-iframealone 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
Always check for null: Client and server hooks may return
nullinitially or when target is unavailable:const client = useClient(iframeRef); if (!client) return null; // Handle null caseUse 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 changesCleanup 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:
- Quickly confirm if Server is online
- Distinguish between "connection failure" and "request timeout"
- 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