1

I have an Express server that receives FormData with an attached FLAC audio file. The code works as expected for several files of varying size (10 - 70MB), but some of them get stuck in the 'file' event and I cannot figure out why this happens. It is even more strange when a file that previously did not fire the file.on('close', => {}) event, as can be seen in the documentation for Busboy, suddenly does so, with the file being successfully uploaded.

To me, this seems completely random, as I have tried this with a dozen files of varying size and content type (audio/flac & audio/x-flac), and the results have been inconsistent. Some files will, however, not work at all, even if I attempt to parse them many times over. Whereas, certain files can be parsed and uploaded, given enough attempts?

Is there some error that I fail to deal with in the 'file' event? I did try to listen to the file.on('error', => {}) event, but there were no errors to be found. Other answers suggest that the file stream must be consumed for the 'close' event to proceed, but I think that file.pipe(fs.createWriteStream(fileObject.filePath)); does that, correct?

Let me know if I forgot to include some important information in my question. This has been bothering me for about a week now, so I am happy to provide anything of relevance to help my chances of overcoming this hurdle.

app.post('/upload', (request, response) => {
  response.set('Access-Control-Allow-Origin', '*');
  const bb = busboy({ headers: request.headers });

  const fields = {};
  const fileObject = {};

  bb.on('file', (_name, file, info) => {
    const { filename, mimeType } = info;

    fileObject['mimeType'] = mimeType;
    fileObject['filePath'] = path.join(os.tmpdir(), filename);
    file.pipe(fs.createWriteStream(fileObject.filePath));

    file.on('close', () => {
      console.log('Finished parsing of file');
    });
  });

  bb.on('field', (name, value) => {
    fields[name] = value;
  });

  bb.on('close', () => {
    bucket.upload(
      fileObject.filePath,
      {
        uploadType: 'resumable',
        metadata: {
          metadata: {
            contentType: fileObject.mimeType,
            firebaseStorageDownloadToken: fields.id
          }
        }
      },
      (error, uploadedFile) => {
        if (error) {
          console.log(error);
        } else {
          db.collection('tracks')
            .doc(fields.id)
            .set({
              identifier: fields.id,
              artist: fields.artist,
              title: fields.title,
              imageUrl: fields.imageUrl,
              fileUrl: `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${uploadedFile.name}?alt=media&token=${fields.id}`
            });

          response.send(`File uploaded: ${fields.id}`);
        }
      }
    );
  });

  request.pipe(bb);
});

UPDATE: 1

I decided to measure the number of bytes that were transferred upon each upload with file.on('data', (data) => {}), just to see if the issue was always the same, and it turns out that this too is completely random.

let bytes = 0;

file.on('data', (data) => {
  bytes += data.length;

  console.log(`Loaded ${(bytes / 1000000).toFixed(2)}MB`);
});

First Test Case: Fenomenon - Sleepy Meadows Of Buxton

Source: https://fenomenon.bandcamp.com/track/sleepy-meadows-of-buxton

  • Size: 30.3MB
  • Codec: FLAC
  • MIME: audio/flac

Results from three attempts:

  1. Loaded 18.74MB, then became stuck
  2. Loaded 5.05MB, then became stuck
  3. Loaded 21.23MB, then became stuck

Second Test Case: Almunia - New Moon

Source: https://almunia.bandcamp.com/track/new-moon

  • Size: 38.7MB
  • Codec: FLAC
  • MIME: audio/flac

Results from three attempts:

  1. Loaded 12.78MB, then became stuck
  2. Loaded 38.65, was successfully uploaded!
  3. Loaded 38.65, was successfully uploaded!

As you can see, the behavior is unpredictable to say the least. Also, those two successful uploads did playback seamlessly from Firebase Storage, so it really worked as intended. What I cannot understand is why it would not always work, or at least most of the time, excluding any network-related failures.

UPDATE: 2

I am hopelessly stuck trying to make sense of the issue, so I have now created a scenario that closely resembles my actual project, and uploaded the code to GitHub. It is pretty minimal, but I did add some additional libraries to make the front-end pleasant to work with.

There is not much to it, other than an Express server for the back-end and a simple Vue application for the front-end. Within the files folder, there are two FLAC files; One of them is only 4.42MB to prove that the code does sometimes work. The other file is much larger at 38.1MB to reliably illustrate the problem. Feel free to try any other files.

Note that the front-end must be modified to allow files other than FLAC files. I made the choice to only accept FLAC files, as this is what I am working with in my actual project.

Malaco
  • 25
  • 5
  • I ran some tests with these options enabled, but unfortunately that made no difference: `file.pipe(fs.createWriteStream(fileObject.filePath, { autoClose: true, emitClose: true }));`. Does `createWriteStream()` not automatically fire the `'close'` event? – Malaco Dec 26 '21 at 15:23
  • I see what you are saying, but the stream does auto-close sometimes, so it should work. I have updated my original question with some examples that illustrate just how unpredictable my implementation is. – Malaco Dec 27 '21 at 00:04
  • Related? https://github.com/mscdex/busboy/issues/264 - tl:dr is listen for the `finish` event on the writeStream returned from `createWriteStream()` _before_ you call `bucket.upload()`. Why? Not all the data has been flushed from the buffer before you attempt to use the result. – Randy Casburn Dec 27 '21 at 01:47
  • That makes sense. I now wait for `createWriteStream()` to emit the `'finish'` event. The problem seems to be that this event is not guaranteed to occur, which may be the root of the issue. Sometimes, there is no event, nor any errors. My code: `file.pipe(fs.createWriteStream(fileObject.filePath)).on('finish', () => {});`. – Malaco Dec 27 '21 at 20:31
  • So next thought is that the `.pipe()` `readable` is signally `end()` and thus closing the `writable` before the writable is finished? You can test this by adding `{end: false}` as the second parameter of your call to `.pipe()`. Just be aware that this is the case that will not close the writable when there is an error condition in the `readable` – Randy Casburn Dec 27 '21 at 21:08
  • If I have understood you correctly, the change I should make is to turn `request.pipe(bb);` into `request.pipe(bb, { end: false });`. I have now tried that with a few files, but unfortunately, the problem persists. – Malaco Dec 27 '21 at 22:01
  • totally perplexed. The next thing I would suggest is to remove the pipe and write the files manually. There is no way to avoid the incomplete file thing that way. – Randy Casburn Dec 28 '21 at 00:59
  • I have now uploaded a scenario that illustrates the problem: https://github.com/MihkelPajunen/busboy-problem. Feel free to have a look if you have the time to spare. Thank you very much for the help thus far. It has really helped me understand the problem in more detail. – Malaco Dec 28 '21 at 15:56
  • Look for your pull request. – Randy Casburn Dec 29 '21 at 04:04
  • Thank you so much, @RandyCasburn! It works perfectly now. I would be happy to accept your answer. – Malaco Dec 29 '21 at 10:22

1 Answers1

0

You'll need to write the file directly when BusBoy emits the file event.

It seems there is a race condition if you rely on BusBoy that prevents the file load from being completed. If you load it in the file event handler then it works fine.

app.post('/upload', (request, response) => {
  response.set('Access-Control-Allow-Origin', '*');
  const bb = busboy({
    headers: request.headers
  });
  const fileObject = {};
  let bytes = 0;
  bb.on('file', (name, file, info) => {
    const {
      filename,
      mimeType
    } = info;
    fileObject['mimeType'] = mimeType;
    fileObject['filePath'] = path.join(os.tmpdir(), filename);
    const saveTo = path.join(os.tmpdir(), filename);
    const writeStream = fs.createWriteStream(saveTo);
    file.on('data', (data) => {
      writeStream.write(data);
      console.log(`Received: ${((bytes += data.length) / 1000000).toFixed(2)}MB`);
    });
    file.on('end', () => {
      console.log('closing writeStream');
      writeStream.close()
    });
  });
  bb.on('close', () => {
    console.log(`Actual size is ${(fs.statSync(fileObject.filePath).size / 1000000).toFixed(2)}MB`);
    console.log('This is where the file would be uploaded to some cloud storage server...');
    response.send('File was uploaded');
  });
  bb.on('error', (error) => {
    console.log(error);
  });
  request.pipe(bb);
});
Randy Casburn
  • 13,840
  • 1
  • 16
  • 31