TL;DR: I expected the fetch
API to resolve once the full HTTP response header was sent, but it is taking 8000ms for some reason.
I have a server using Express to respond to HTTP requests. It doesn't give a single response, but rather it makes multiple write
calls over time to give status updates. In the browser, the HTTP request is made using the fetch API. The response body is iterated over asynchronously.
All of this works fine except for one issue. The time it takes to await the response from the fetch call is about 8 seconds. I know that the first chunk sent by the server does not take 8 seconds. I have even compared to XHR, which is taking less than 50ms for the progress event to fire. Is there anything I can do to make the initial fetch promise resolve sooner?
app.post("/bulk-update", (req, res) => {
buildTask(req.body, 5, (status) => {
const completed = status.success + status.error;
const remaining = status.total - completed;
if (remaining === 0) {
res.write(`${JSON.stringify(status)}\n`);
res.end();
return;
}
if (completed % 10 === 0) {
res.write(`${JSON.stringify(status)}\n`);
}
});
});
let jobStartTime = Date.now();
const response = await fetch(`/bulk-update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body,
});
console.log("end request", Date.now() - jobStartTime); // <-- this is logging 8000ms
const stream = response.body.pipeThrough(new TextDecoderStream());
let buffer = "";
for await (const chunk of stream) {
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.at(-1);
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i];
const status = JSON.parse(line);
updateProgress(status);
}
if (buffer.trim().length > 0) {
const status = JSON.parse(buffer);
updateProgress(status);
}
}
I was able to get the fetch to resolve sooner by setting the Content-Type
header to text/event-stream
in the response from the server. It seems that if the fetch
does not see this header, it takes longer to resolve.
app.post("/bulk-update", (req, res) => {
res.setHeader('Content-Type', 'text/event-stream'); // <-- this is the line the fixed it
res.setHeader('Connection', 'keep-alive'); // Not needed for quick fetch, just is accurate
buildTask(req.body, 5, (status) => {
const completed = status.success + status.error;
const remaining = status.total - completed;
if (remaining === 0) {
res.write(`${JSON.stringify(status)}\n`);
res.end();
return;
}
if (completed % 10 === 0) {
res.write(`${JSON.stringify(status)}\n`);
}
});
});
I didn't see anything in the Fetch Standard about this. I also tested on Chrome and fetch resolved quickly without needing the header. As far as I can tell, this is an implementation detail specifically with Firefox.