Responses & Errors

Streaming and Files

Stream binary data, pipe readable streams, return Blob/Buffer payloads, and serve local files with path traversal security.

Taser.js includes first-class helpers for streaming responses, binary payloads, and secure file downloads without manual buffer management.


Serving Local Files (file)

Use file() imported from @taserjs/router/stream to serve files from disk. It automatically looks up the correct MIME type based on file extension and streams the file using Node.js read streams:

src/routes/downloads/$file.get.ts
import { file } from "@taserjs/router/stream";
import { z } from "zod";
import { t } from "@taserjs/router";
import path from "node:path";

export default t
  .get("/downloads/:file")
  .params(z.object({ file: z.string() }))
  .handler((ctx) => {
    const publicStorageDir = path.resolve(process.cwd(), "storage/public");

    // Serves the requested file safely constrained to publicStorageDir:
    return file(ctx.params.file, {
      root: publicStorageDir,
    });
  });

Path Traversal Protection

When root is specified, Taser.js validates that the resolved file path does not escape the boundary directory, protecting your application against ../ directory traversal attacks.


Piping Readable Streams (pipe)

To stream dynamically generated data, AI completions, or proxy upstream streams, use pipe():

src/routes/ai/generate.post.ts
import { pipe } from "@taserjs/router/stream";
import { t } from "@taserjs/router";

export default t.post("/ai/generate").handler(async (ctx) => {
  const encoder = new TextEncoder();

  const customStream = new ReadableStream({
    async start(controller) {
      for (const word of ["Streaming", " live", " AI", " response", "..."]) {
        controller.enqueue(encoder.encode(word));
        await new Promise((resolve) => setTimeout(resolve, 200));
      }
      controller.close();
    },
  });

  return pipe(customStream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Transfer-Encoding": "chunked",
    },
  });
});

Binary Payloads (buffer and blob)

When returning generated images, PDFs, or ZIP archives:

src/routes/export/pdf.get.ts
import { buffer } from "@taserjs/router/stream";
import { t } from "@taserjs/router";

export default t.get("/export/pdf").handler(async (ctx) => {
  const pdfBytes: Uint8Array = await generateInvoicePdf(ctx.query);

  return buffer(pdfBytes, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": 'attachment; filename="invoice.pdf"',
    },
  });
});

Server-Sent Events (SSE)

You can build Server-Sent Events (SSE) streams by piping formatted SSE message events:

src/routes/events/notifications.get.ts
import { stream } from "@taserjs/router/stream";
import { t } from "@taserjs/router";

export default t.get("/events/notifications").handler((ctx) => {
  const encoder = new TextEncoder();

  const eventStream = new ReadableStream({
    start(controller) {
      const interval = setInterval(() => {
        const payload = `data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`;
        controller.enqueue(encoder.encode(payload));
      }, 1000);

      ctx.request.signal.addEventListener("abort", () => {
        clearInterval(interval);
        controller.close();
      });
    },
  });

  return stream.pipe(eventStream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
});

Next Steps