Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending file with Fastify server prematurely marks transfer as finished #16037

Open
jkscx opened this issue Dec 29, 2024 · 0 comments
Open

Sending file with Fastify server prematurely marks transfer as finished #16037

jkscx opened this issue Dec 29, 2024 · 0 comments
Labels
bug Something isn't working needs investigate Needs to be investigated to find the root cause node:http

Comments

@jkscx
Copy link

jkscx commented Dec 29, 2024

What version of Bun is running?

1.1.42+50eec0025

What platform is your computer?

Linux 6.8.0-39-generic x86_64 unknown

What steps can reproduce the bug?

I was trying to send a file with a Fastify server and upon completion of the file's transfer have some post-transfer action execute, but realized that the post-transfer action was executing too quickly. The file transfer was taking at least 10 seconds, but the post-transfer action was executed almost immediately after starting the transfer...

Upon extensive debugging I have realized that it's a bug in Bun because the same code correctly works with Node. The problem lies in the fact that the file stream which is sent prematurely closes (which does not happen in Node). The file does get transferred in it's entirety, but the file stream which is used to read from the file gets closed before that and I think that makes Fastify think that the transfer finished.

For now I'm switching back to Node, but here's a minimal reproducible example which showcases the bug in case someone is interested in fixing it.

import * as fs from "node:fs"

import Fastify from 'fastify'
import { Readable, Transform, Writable } from 'node:stream';

// helper
async function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

const fastifyServer = Fastify();
fastifyServer.post("/get-file-stream", async (req, reply) => {
    console.time();
    console.info(new Date(), "Request received!");

    // print "ping" every second while sending data
    const pingTaskId = setInterval(() => {
        console.log(new Date(), "ping");
    }, 1000);

    // file can be created via 'head -c 15MB /dev/urandom > ./random-15mb.txt'
    const fileStream = fs.createReadStream('./random-15mb.txt');
    fileStream.on('end', () => {console.info(new Date(), "💧 Stream finished");})
    fileStream.on('close', () => {
        console.info(new Date(), "💧 Stream CLOSED");
        console.timeEnd();
        console.log(); // newline to visually separate stuff
    })

    reply.status(200).send(fileStream);
    await reply; // wait until the file is fully sent
    console.info(new Date(), "📩 Reply awaited");
    clearInterval(pingTaskId); // stop printing "ping"
});

await fastifyServer.listen({port: 42105, host: "0.0.0.0"});
console.log("Server listening...");

One can curl the server to trigger the bug:

curl -X POST http://0.0.0.0:42105/get-file-stream > /dev/null

NOTE: the file transfer needs to be longer so that one can actually notice that the post-transfer action is triggered before the transfer is finished, so either a slow network is required over which the file is sent or (if querying the server locally) the file needs to be quite large.

What is the expected behavior?

Running with Node produces:

$ node ./test2.js
Server listening...
2024-12-29T00:36:51.479Z Request received!
2024-12-29T00:36:52.483Z ping
2024-12-29T00:36:53.483Z ping
2024-12-29T00:36:54.483Z ping
2024-12-29T00:36:55.483Z ping
2024-12-29T00:36:56.406Z 💧 Stream finished
2024-12-29T00:36:56.408Z 📩 Reply awaited
2024-12-29T00:36:56.408Z 💧 Stream CLOSED
default: 4.930s

Notice the 4 pings while the file is transferred and how the transfer takes 4.9 sec.

What do you see instead?

Running with Bun produces:

$ bun run test2.js
Server listening...
2024-12-29T00:35:52.801Z Request received!
2024-12-29T00:35:52.877Z 💧 Stream finished
2024-12-29T00:35:52.879Z 📩 Reply awaited
2024-12-29T00:35:52.879Z 💧 Stream CLOSED
[79.83ms] default

Notice that there's no pings and how the transfer allegedly took only 79 millisec (which is obviously wrong), the transfer was still ongoing for 4 more sec after the message got printed.

Additional information

Platform

This is just for Linux, on MacOS the bug changes. Instead of the stream being closed prematurely, it's stays open until the transfer actually finishes, but another bug occurs...

Sending a stream (as in /dev/random) and aborting causes the server to never close the stream

Try adding the below endpoint to the server, curling it and then Ctrl-C curl. Node closes the stream normally, while it hangs forever in Bun.

fastifyServer.post("/get-infinite-file-stream", async (req, reply) => {
    console.time();
    console.info(new Date(), "Request received!");
    // print "ping" every second while sending data
    const pingTaskId = setInterval(() => {
        console.log(new Date(), "ping");
    }, 1000);

    const fileStream = fs.createReadStream("/dev/random");
    fileStream.on('end', () => {console.info(new Date(), "💧 Stream finished");});
    fileStream.on('close', () => {
        console.info(new Date(), "💧 Stream CLOSED");
        console.timeEnd();
        console.log(); // newline to visually separate stuff
    });

    reply.status(200).send(fileStream);
    await reply;
    console.info(new Date(), "📩 Reply awaited");
    clearInterval(pingTaskId); // stop printing "ping"
});

Sending a generated stream works normally (as in Node)

fastifyServer.post("/get-generator-stream", async (req, reply) => {
    console.time();
    console.info(new Date(), "Request received!");
    // print "ping" every second while sending data
    const pingTaskId = setInterval(() => {
        console.log(new Date(), "ping");
    }, 1000);

    async function* generate() {
        await sleep(1000);
        yield '1000\n';
        await sleep(1000);
        yield '2000\n'
        await sleep(3000);
        yield '5000\n'
        await sleep(1000);
        yield 'END\n'
    }
    const fileStream = new Readable.from(generate());
    fileStream.on('end', () => {console.info(new Date(), "💧 Stream finished");})
    fileStream.on('close', () => {
        console.info(new Date(), "💧 Stream CLOSED");
        console.timeEnd();
        console.log(); // newline to visually separate stuff
    });

    reply.status(200).send(fileStream);
    await reply;
    console.log(new Date(), `📩 Reply awaited`);
    clearInterval(pingTaskId); // stop printing "ping"
});
$ bun run test2.js
Server listening...
2024-12-29T01:31:21.644Z Request received!
2024-12-29T01:31:22.648Z ping
2024-12-29T01:31:23.646Z ping
2024-12-29T01:31:24.647Z ping
2024-12-29T01:31:25.647Z ping
2024-12-29T01:31:26.648Z ping
2024-12-29T01:31:27.649Z ping
2024-12-29T01:31:27.665Z 💧 Stream finished
2024-12-29T01:31:27.673Z 📩 Reply awaited
2024-12-29T01:31:27.674Z 💧 Stream CLOSED
[6.03s] default
@jkscx jkscx added bug Something isn't working needs triage labels Dec 29, 2024
@DonIsaac DonIsaac added node:http needs investigate Needs to be investigated to find the root cause and removed needs triage labels Dec 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs investigate Needs to be investigated to find the root cause node:http
Projects
None yet
Development

No branches or pull requests

2 participants