When integrating n8n-io/n8n (v2.38+) into modern web frontends, a common architecture is using n8n as an autonomous agent orchestrator while serving UI over React 19 and @ai-sdk/react. However, connecting n8n's Webhook streaming node directly to a client typewriter interface exposes subtle protocol mismatches.
By default, n8n emits chunked HTTP or standard Server-Sent Events (SSE). If consumed naively in useChat, reverse proxies (Nginx/Cloudflare) buffer chunks until a 4KB boundary is reached, destroying the real-time typewriter effect. Furthermore, unhandled connection drops or manual stream cancellations leave dangling execution threads on the n8n runner.
To normalize the stream for Vercel AI SDK and ensure client-driven cancellation propagates downstream, place a lightweight App Router edge route between React 19 and your n8n instance:
// app/api/chat/route.ts
import { createDataStreamResponse } from 'ai';
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const controller = new AbortController();
req.signal.addEventListener('abort', () => controller.abort());
return createDataStreamResponse({
execute: async (dataStream) => {
const upstream = await fetch(process.env.N8N_WEBHOOK_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Accel-Buffering': 'no', // Disable upstream Nginx proxy buffering
},
body: JSON.stringify({ messages }),
signal: controller.signal,
});
if (!upstream.ok || !upstream.body) {
throw new Error(`n8n upstream rejected with status ${upstream.status}`);
}
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Pipe normalized token chunk into the AI SDK text stream
dataStream.writeText(chunk);
}
},
});
}
On the client side, bind @ai-sdk/react directly. React 19's concurrent rendering prevents UI hitching during rapid burst emissions:
'use client';
import { useChat } from '@ai-sdk/react';
export function AgentChat() {
const { messages, input, handleInputChange, handleSubmit, is, stop } = useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-full max-w-2xl mx-auto p-4">
<div className="flex-1 overflow-y-auto space-y-3">
{messages.map((m) => (
<div key={m.id} className={m.role === 'user' ? 'text-right font-medium' : 'text-left text-slate-800'}>
{m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input value={input} onChange={handleInputChange} placeholder="Prompt workflow..." className="border p-2 flex-1 rounded" />
{is ? (
<button type="button" onClick={stop} className="px-4 py-2 bg-red-600 text-white rounded">Stop</button>
) : (
<button type="submit" className="px-4 py-2 bg-black text-white rounded">Send</button>
)}
</form>
</div>
);
}
Always forward req.signal to n8n's incoming fetch. Without this handshake, clicking "Stop" in the client aborts the local connection while n8n continues wasting LLM tokens executing orphaned background nodes.
Demonstrates connecting Vercel AI SDK chat interfaces directly to B-Lost's unbuffered SSE relay, eliminating reverse-proxy buffering delays.
Disclosure: Multi-model API relays and compute for this evaluation are sponsored by b-lost.com — an AI gateway offering 0.8x official pricing, native prompt caching, and zero user-data retention. All observations reflect independent developer testing.