I have a long running process (~30sec) that calls several external apis. That process should run on the server in my next.js app.
I created an /api
route and things work, I however want to give the user feedback on the process that is composed of several subtasks. E.g. the user should see:
I tried several ways to do it:
api/hello.js
export default async function handler(req, res) {
res.writeHead(200, { "Content-Type": "text/plain" });
res.write(JSON.stringify({ data: "Step1" }));
res.write(JSON.stringify({ data: "Step2" }));
res.write(JSON.stringify({ data: "Step3" }));
res.end();
}
client
const response = await fetch("/api/hello", {
method: "POST",
body: JSON.stringify(config),
keepalive: true,
mode: "cors",
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log('Received:', decoder.decode(value));
}
console.log("Response fully received");
The above code does output
received {"data":"Step1"}{"data":"Step2"}{"data":"Step3"}
"Response fully received"
I was expecting that chunks are logged after each other and not at once?
How can I fix 1) or is 2) suited or is there an option 3) I am not seeing?
I think to use server-sent events you need to use text/event-stream
MIME type instead. I just made a quick reproduction locally and it works just fine. I could not make a Codesandbox repro because it's extremely laggy for me after an update, but I will add a link here if it starts to work.
Basically I just used these 2 files:
pages/api/endpoint.ts
:
import type { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, {
Connection: "keep-alive",
// I saw somewhere that it needs to be set to 'none' to work in production on Vercel
"Content-Encoding": "none",
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
});
let count = 0;
res.write(`${JSON.stringify({ step: 0 })}`);
const intervalId = setInterval(() => {
count++;
res.write(`${JSON.stringify({ step: count })}`);
if (count === 100) {
clearInterval(intervalId);
res.end();
}
}, 5000);
res.on("close", () => {
clearInterval(intervalId);
res.end();
});
}
And this is a client component for Next.js 13:
"use client";
import { useEffect, useState } from "react";
export function ClientComponent() {
const [state, setState] = useState({ step: 0 });
useEffect(() => {
const callApi = async () => {
const data = await fetch("/api/endpoint", { method: "post" });
const stream = data.body;
if (!stream) {
return;
}
for await (const message of streamToJSON(stream)) {
setState(JSON.parse(message));
}
};
callApi();
}, []);
return (
<main>
<div>Current step is: {state.step}</div>
</main>
);
}
async function* streamToJSON(
data: ReadableStream<Uint8Array>
): AsyncIterableIterator<string> {
const reader = data.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
if (value) {
try {
yield decoder.decode(value);
} catch (error) {
console.error(error);
}
}
}
}