Skip to content

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

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"}}\n

HTTPChannel operates in two distinct modes:

ModeConfigurationUse Case
ClientHas baseUrlSend commands to a remote server
ServerNo baseUrlReceive commands from clients

Client mode sends HTTP POST requests with Accept: application/x-ndjson header and reads streaming NDJSON responses.

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)
OptionTypeRequiredDescription
idstringYesUnique channel identifier
baseUrlstringYes (client)Server URL (e.g., https://api.example.com)
commandPrefixstringNoPrefix added to remote command IDs
timeoutnumberNoRequest timeout in milliseconds (default: 30000)

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 prefix
await registry.executeCommand('api.user.create', { name: 'John' })
// Sends to server as 'user.create' (prefix stripped)

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
})

Each middleware receives a context object with:

PropertyTypeDescription
urlstringThe request URL
headersHeadersHTTP headers (modifiable)
messageCommandMessageThe request message
abortControllerAbortControllerController to abort the request
channel.use(async (ctx, next) => {
const token = await getAuthToken()
ctx.headers.set('Authorization', `Bearer ${token}`)
return next()
})
channel.use(async (ctx, next) => {
console.log('Request:', ctx.url, ctx.message)
const result = await next()
console.log('Response:', result)
return result
})
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)))
}
}
})

Middleware can be chained using fluent syntax:

channel
.use(authMiddleware)
.use(loggingMiddleware)
.use(retryMiddleware)
await registry.registerChannel(channel)

Server mode receives HTTP requests and processes them via the registry.

import { HTTPChannel } from '@coralstack/cmd-ipc'
const channel = new HTTPChannel({
id: 'http-server',
// No baseUrl = server mode
})
await registry.registerChannel(channel)

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)

handleRequest() has specific error behaviors:

ConditionBehavior
Invalid message schemaThrows InvalidMessageError with validation issues
Forbidden message typeThrows Error: Message type ${message.type} is not allowed
Channel closedThrows Error: Channel is closed
Client modeThrows Error: handleRequest() can only be used in server mode
Request timeoutWrites timeout error to stream and closes it

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 preflight
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders })
}
// Add to streaming responses
const stream = channel.handleRequest(body)
return new Response(stream, {
headers: {
'Content-Type': 'application/x-ndjson',
...corsHeaders
}
})

To support HTTPChannel and the CLI to generate local schemas for clients, your server should support two endpoints:

EndpointMethodPurpose
/cmdPOSTExecute commands (authenticated)
/cmds.jsonGETSchema 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.

Cloudflare Workers don’t allow async operations at global scope:

// ❌ Wrong - async at global scope
const registry = new CommandRegistry({ id: 'worker' })
await registry.registerChannel(channel)
// ✅ Correct - lazy initialization
let 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()
// ...
}
}

If commands frequently timeout:

  1. Increase the timeout value
  2. Check server response times
  3. Verify network connectivity
  4. Check for server-side errors