Skip to content

finish is never emitted when bytes arrive after the closing boundary in a separate write #228

Closed

Description

@dinwwwh

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

@fastify/[email protected]

Plugin version

No response

Node.js version

v24.18.1

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

ubuntu 26.04

Description

Busboy never emits finish if any bytes are written after the closing boundary delimiter (--boundary--) in a separate write() call.

RFC 2046 allows an epilogue after the closing delimiter. In a streamed request the epilogue can arrive in a later TCP segment, so it reaches busboy as its own write() after the body has already been fully parsed.

What happens in that case:

  • field fires as expected
  • finish is never emitted
  • error is never emitted either, so the stream just hangs

If the same trailing bytes are part of the same chunk as the body, everything works. The hang only happens when they arrive in a later tick.

Steps to reproduce:

import { Busboy } from "@fastify/busboy";
import { setImmediate as nextTick } from "node:timers/promises";

const boundary = "boundary";
const busboy = new Busboy({
  headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
});

busboy.on("field", (name, value) => {
  console.log("field:", name, "=", value);
});

async function run() {
  // A complete, valid multipart body, ending with the closing delimiter `--boundary--`.
  const body =
    `--${boundary}\r\n` +
    `Content-Disposition: form-data; name="field"\r\n` +
    `\r\n` +
    `value\r\n` +
    `--${boundary}--\r\n`;

  // Write the whole valid body first.
  busboy.write(body);

  // Yield once, so busboy finishes parsing and its internal Dicer ends/destroys,
  // then write the trailing bytes (RFC 2046 "epilogue"). This is what a streamed
  // request produces when the epilogue arrives in a later TCP segment.
  await nextTick();
  busboy.write("epilogue");

  busboy.end();
}

const timer = setTimeout(() => {
  console.log("HANG (finish never fired)");
  process.exit(1);
}, 2000);

busboy.on("finish", () => {
  clearTimeout(timer);
  console.log("finish");
  process.exit(0);
});
busboy.on("error", (err) => {
  clearTimeout(timer);
  console.log("error:", err.message);
  process.exit(1);
});

run();

Link to code that reproduces the bug

inside description

Expected Behavior

Busboy should emit finish after end(). Bytes after the closing delimiter are an epilogue and should be ignored.

If busboy intends to reject them instead, it should emit error rather than hang silently.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions