Unable to download object from an S3 bucket from NextJS

0

In a NextJS 14 project, I have a server-side endpoint to download a pdf file from an S3 bucket:

/api/download/route.ts

import { NextRequest, NextResponse } from "next/server"
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { Readable } from "stream"

// Securely import AWS credentials from environment variables
const S3_REGION = process.env.AWS_S3_REGION
const S3_ACCESS_KEY_ID = process.env.AWS_S3_ACCESS_KEY_ID
const S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY
const S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME

if (
  !S3_REGION ||
  !S3_ACCESS_KEY_ID ||
  !S3_SECRET_ACCESS_KEY ||
  !S3_BUCKET_NAME
) {
  throw new Error("Missing AWS S3 credentials in environment variables!")
}

const s3Client = new S3Client({
  region: S3_REGION,
  credentials: {
    accessKeyId: S3_ACCESS_KEY_ID,
    secretAccessKey: S3_SECRET_ACCESS_KEY,
  },
})

export async function POST(request: NextRequest, response: NextResponse) {
  const bodyString = await request.text()
  const data = JSON.parse(bodyString) // Parse the JSON data
  if (!data || !data.key) {
    return NextResponse.json({ success: false })
  }
  const { key } = data

  // Download the file from S3 using "key"
  try {
    const bucketParams = {
      Bucket: S3_BUCKET_NAME,
      Key: key,
    }
    // Get the object from S3
    const fileStream = await s3Client.send(new GetObjectCommand(bucketParams))

    // Retrieve content type and filename
    const contentType = fileStream.Metadata?.["Content-Type"]
    const filename =
      fileStream.Metadata?.["Content-Disposition"]?.split("=")[1].trim() ?? key

    // Before setting the Content-Type header, ensure that contentType has a value
    if (contentType) {
      response.headers.set("Content-Type", contentType)
    } else {
      // Handle the case where the content type is missing
      console.error("Content type is missing in file metadata")
      return NextResponse.json({
        success: false,
        error: "Missing content type",
      })
    }

    // Set appropriate headers for download
    response.headers.set("Content-Type", contentType)
    response.headers.set(
      "Content-Disposition",
      `attachment; filename=${filename}`,
    )

    // Pipe the S3 object stream to the response
    if (fileStream.Body !== null) {
      if (fileStream.Body instanceof Readable) {
        await fileStream.Body.pipe(response.body)
      } else {
        // Handle non-readable stream type
      }
    } else {
      console.error("File stream body is null!")
      return NextResponse.json({
        success: false,
        error: "Missing file content",
      })
    }

    return NextResponse.json({ success: true, key })
  } catch (error) {
    // Explicitly cast the error to a string
    return NextResponse.json({ success: false, error: String(error) })
  }
}

This endpoint receives the filename as a variable called key and uses it to download the file from the S3 bucket.

The code seems fine but there’s a persistent error on the following line:

await fileStream.Body.pipe(response.body)

That says:

Argument of type 'ReadableStream<Uint8Array> | null' is not assignable to parameter of type 'WritableStream'. Type 'null' is not assignable to type 'WritableStream'.

I am already doing an explicit typecheck on fileStream.Body. What else am I missing?

UPDATE:

I also tried modifying the piping algorithm to this:

const writableStream = NextResponse.writableStream()
fileStream.Body.pipe(writableStream)

But this too fails saying:

Property 'writableStream' does not exist on type 'NextResponse<unknown>'.

asked 10 months ago1.4K views
1 Answer
1

The issue you're encountering is related to typescript's type checking. The response.body expects a WritableStream, but TypeScript cannot infer that fileStream.Body is a ReadableStream. The ReadableStream from @aws-sdk/client-s3 does not directly extend NodeJS.ReadableStream, which is why TypeScript is having trouble understanding its type.

You can try a workaround by creating a custom WritableStream using Node.js's stream.Writable class. Here's how you can do it:

// Import necessary modules
import { Readable } from "stream";
import { Writable } from "stream";
import { NextRequest, NextResponse } from "next/server";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

// Your existing code ...

// Pipe the S3 object stream to the response
if (fileStream.Body !== null) {
  if (fileStream.Body instanceof Readable) {
    // Create a custom writable stream
    const customWritableStream = new Writable({
      write(chunk, encoding, callback) {
        response.write(chunk, encoding); // Write data to the response
        callback(); // Call the callback function to indicate that writing is complete
      },
    });

    // Pipe the S3 object stream to the custom writable stream
    fileStream.Body.pipe(customWritableStream);

    // Return the response
    return NextResponse.json({ success: true, key });
  } else {
    // Handle non-readable stream type
  }
} else {
  // Handle the case where the file stream body is null
}

we're creating a custom writable stream using Node.js's stream.Writable class, which allows us to explicitly define the write function. Then, we pipe the S3 object stream to this custom writable stream and write the data to the response. Finally, we call the callback function to indicate that writing is complete. This should resolve the TypeScript error you're encountering.

Let me know if this helps

profile picture
EXPERT
answered 10 months ago
  • I think we’re on the right path, but now it says: Property 'write' does not exist on type 'NextResponse<unknown>'.

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions