HTTPChannel
HTTPChannel enables command communication over HTTP using the Protocol message format with NDJSON (Newline-Delimited JSON) streaming. It supports both client and server modes, making it ideal for:
- REST API backends
- Cloudflare Workers
- Edge functions
- Microservices communication
NDJSON Streaming
Section titled “NDJSON Streaming”All communication uses NDJSON streaming format. Each message is a JSON object followed by a newline character (\n). This enables efficient streaming responses and better memory usage for large payloads.
{"type":"execute.command.response","thid":"abc-123","response":{"ok":true,"result":"Hello"}}\nTwo Modes
Section titled “Two Modes”HTTPChannel operates in two distinct modes:
| Mode | Configuration | Use Case |
|---|---|---|
| Client | Has baseUrl | Send commands to a remote server |
| Server | No baseUrl | Receive commands from clients |
Client Mode
Section titled “Client Mode”Client mode sends HTTP POST requests with Accept: application/x-ndjson header and reads streaming NDJSON responses.
Configuration
Section titled “Configuration”import { HTTPChannel } from '@coralstack/cmd-ipc'
const channel = new HTTPChannel({ id: 'api', baseUrl: 'https://api.example.com', commandPrefix: 'api', // Optional: prefix for remote commands timeout: 30000, // Optional: request timeout in ms (default: 30000)})
await registry.registerChannel(channel)Configuration Options
Section titled “Configuration Options”| Option | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique channel identifier |
baseUrl | string | Yes (client) | Server URL (e.g., https://api.example.com) |
commandPrefix | string | No | Prefix added to remote command IDs |
timeout | number | No | Request timeout in milliseconds (default: 30000) |
Command Prefix
Section titled “Command Prefix”The commandPrefix option helps organize commands from different sources:
// Without prefix:// Remote 'user.create' → local 'user.create'
// With prefix 'api':// Remote 'user.create' → local 'api.user.create'
const channel = new HTTPChannel({ id: 'api', baseUrl: 'https://api.example.com', commandPrefix: 'api',})
// Execute with prefixawait registry.executeCommand('api.user.create', { name: 'John' })// Sends to server as 'user.create' (prefix stripped)Middleware
Section titled “Middleware”HTTPChannel supports middleware for modifying requests before they’re sent. Use the use() method to add middleware functions that execute in the order they’re added.
import { HTTPChannel } from '@coralstack/cmd-ipc'
const channel = new HTTPChannel({ id: 'api', baseUrl: 'https://api.example.com',})
// Add middleware using use()channel.use(async (ctx, next) => { // Modify request before sending ctx.headers.set('Authorization', `Bearer ${getToken()}`)
// Call next() to continue the chain const result = await next()
// Optionally modify the response return result})Middleware Context
Section titled “Middleware Context”Each middleware receives a context object with:
| Property | Type | Description |
|---|---|---|
url | string | The request URL |
headers | Headers | HTTP headers (modifiable) |
message | CommandMessage | The request message |
abortController | AbortController | Controller to abort the request |
Authentication Middleware
Section titled “Authentication Middleware”channel.use(async (ctx, next) => { const token = await getAuthToken() ctx.headers.set('Authorization', `Bearer ${token}`) return next()})Logging Middleware
Section titled “Logging Middleware”channel.use(async (ctx, next) => { console.log('Request:', ctx.url, ctx.message) const result = await next() console.log('Response:', result) return result})Retry Middleware
Section titled “Retry Middleware”channel.use(async (ctx, next) => { for (let i = 0; i < 3; i++) { try { return await next() } catch (err) { if (i === 2) throw err await new Promise(r => setTimeout(r, 1000 * (i + 1))) } }})Chaining Middleware
Section titled “Chaining Middleware”Middleware can be chained using fluent syntax:
channel .use(authMiddleware) .use(loggingMiddleware) .use(retryMiddleware)
await registry.registerChannel(channel)Server Mode
Section titled “Server Mode”Server mode receives HTTP requests and processes them via the registry.
Configuration
Section titled “Configuration”import { HTTPChannel } from '@coralstack/cmd-ipc'
const channel = new HTTPChannel({ id: 'http-server', // No baseUrl = server mode})
await registry.registerChannel(channel)Handling Requests
Section titled “Handling Requests”Use handleRequest() to process incoming HTTP requests. The method validates the message schema and processes allowed message types (list.commands.request, list.commands.response, execute.command.request, execute.command.response).
handleRequest() returns a ReadableStream that can be used directly as the HTTP response body. The stream uses NDJSON format.
import express from 'express'import { Readable } from 'stream'import { InvalidMessageError, publishSchemaDoc } from '@coralstack/cmd-ipc'
const app = express()app.use(express.json())
app.post('/cmd', (req, res) => { try { const stream = channel.handleRequest(req.body)
res.setHeader('Content-Type', 'application/x-ndjson') res.setHeader('Transfer-Encoding', 'chunked')
// Convert Web ReadableStream to Node.js stream Readable.fromWeb(stream).pipe(res) } catch (err) { if (err instanceof InvalidMessageError) { return res.status(400).json({ error: 'Invalid message format', issues: err.issues }) } if (err instanceof Error && err.message.includes('is not allowed')) { return res.status(403).json({ error: err.message }) } throw err }})
app.get('/cmds.json', (req, res) => { res.json(publishSchemaDoc(registry.listCommands()))})
app.listen(3000)import { InvalidMessageError, publishSchemaDoc } from '@coralstack/cmd-ipc'
export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url)
if (request.method === 'POST' && url.pathname === '/cmd') { try { const body = await request.json() const stream = channel.handleRequest(body)
return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked' } }) } catch (err) { if (err instanceof InvalidMessageError) { return new Response(JSON.stringify({ error: 'Invalid message format' }), { status: 400, headers: { 'Content-Type': 'application/json' } }) } if (err instanceof Error && err.message.includes('is not allowed')) { return new Response(JSON.stringify({ error: err.message }), { status: 403, headers: { 'Content-Type': 'application/json' } }) } throw err } }
if (request.method === 'GET' && url.pathname === '/cmds.json') { return new Response(JSON.stringify(publishSchemaDoc(registry.listCommands())), { headers: { 'Content-Type': 'application/json' } }) }
return new Response('Not Found', { status: 404 }) }}import { createServer } from 'http'import { Readable } from 'stream'import { InvalidMessageError, publishSchemaDoc } from '@coralstack/cmd-ipc'
const server = createServer(async (req, res) => { if (req.method === 'POST' && req.url === '/cmd') { const chunks: Buffer[] = [] for await (const chunk of req) chunks.push(chunk) const body = JSON.parse(Buffer.concat(chunks).toString())
try { const stream = channel.handleRequest(body)
res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked' })
// Convert Web ReadableStream to Node.js stream Readable.fromWeb(stream).pipe(res) } catch (err) { if (err instanceof InvalidMessageError) { res.writeHead(400, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Invalid message format' })) return } if (err instanceof Error && err.message.includes('is not allowed')) { res.writeHead(403, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: err.message })) return } throw err } return }
if (req.method === 'GET' && req.url === '/cmds.json') { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify(publishSchemaDoc(registry.listCommands()))) return }
res.writeHead(404) res.end('Not Found')})
server.listen(3000)Error Handling
Section titled “Error Handling”handleRequest() has specific error behaviors:
| Condition | Behavior |
|---|---|
| Invalid message schema | Throws InvalidMessageError with validation issues |
| Forbidden message type | Throws Error: Message type ${message.type} is not allowed |
| Channel closed | Throws Error: Channel is closed |
| Client mode | Throws Error: handleRequest() can only be used in server mode |
| Request timeout | Writes timeout error to stream and closes it |
CORS Configuration
Section titled “CORS Configuration”For browser clients, configure CORS headers:
const corsHeaders = { 'Access-Control-Allow-Origin': '*', // Or specific origin 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type',}
// Handle preflightif (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders })}
// Add to streaming responsesconst stream = channel.handleRequest(body)return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson', ...corsHeaders }})Endpoints
Section titled “Endpoints”To support HTTPChannel and the CLI to generate local schemas for clients, your server should support two endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/cmd | POST | Execute commands (authenticated) |
/cmds.json | GET | Schema discovery for CLI (usually unauthenticated) |
Use the publishSchemaDoc() helper function to generate the correct schema document format for /cmds.json. It automatically filters out private commands and formats the response for the CLI.
Common Issues
Section titled “Common Issues”Cloudflare Workers Global Scope
Section titled “Cloudflare Workers Global Scope”Cloudflare Workers don’t allow async operations at global scope:
// ❌ Wrong - async at global scopeconst registry = new CommandRegistry({ id: 'worker' })await registry.registerChannel(channel)
// ✅ Correct - lazy initializationlet registry: CommandRegistry | null = null
async function getRegistry() { if (!registry) { registry = new CommandRegistry({ id: 'worker' }) await registry.registerChannel(channel) } return registry}
export default { async fetch(request) { const reg = await getRegistry() // ... }}Timeout Errors
Section titled “Timeout Errors”If commands frequently timeout:
- Increase the timeout value
- Check server response times
- Verify network connectivity
- Check for server-side errors