Search code examples
reactjsnext.jsvercel

Next 14 | App Router| Vercel Deployment - Can't find the public folder


I have implemented a file downloading api endpoint in next js. It lets the user download files from secluded folders that are within the public folder of next JS. Here is my code:

export async function GET(request: Request, { params }: GetParams) {
  try {
    const filename = params.filename;

    if (!filename) {
      throw new Error('filename parameter should not be empty.');
    }
    let folderPath = '';
    if (filename.startsWith('myFile')) {
      folderPath = 'public/myFiles';
    } else if (filename.startsWith('yourFile')) {
      folderPath = 'public/yourFiles';
    } else if (filename.startsWith('theirFile')) {
      folderPath = 'public/theirFiles';
    } else {
      throw new Error(`Unknown filename type: ${filename}`);
    }

    const files = await fs.promises.readdir(
      path.join(process.cwd(), folderPath)
    );

    for (const file of files) {
      if (file.startsWith(filename)) {
        const buffer = await readFile(
          path.join(process.cwd(), folderPath, file)
        );
        //Create headers
        const headers = new Headers();
        headers.append('content-disposition', `attachment; filename="${file}"`);
        headers.append('Content-Type', 'image/png');

        return new Response(buffer, {
          status: 200,
          headers,
        });
      }
    }
    return new Response(`File not found: ${filename}`, {
      status: 404,
    });
  } catch (error: any) {
    console.log(error);
    return new Response(`Internal server error`, {
      status: 500,
    });
  }
}

This works perfect in dev environment. However this fails in production with the error:

[Error: ENOENT: no such file or directory, scandir '/var/task/public/myFiles'] { errno: -2, code: 'ENOENT', syscall: 'scandir', path: '/var/task/public/myFiles' }

  • Next version being used by me: "14.1.0"
  • I am using App Router.
  • I am using Typescript (evident from code posted by me).

Clearly either,

  • NextJs is using some different directory structure in the production version.
  • Or I have written the API handler function in a wrong way (the way I am using path?).

Search on the web is either filled with old "page router" references, or they have some weird concepts that confuses further. Is there a way to see the directory structure that vercel uses for deploying the application? Any pointer or help is appreciated.


Solution

  • I was able to solve this using the following post:

    https://medium.com/@boris.poehland.business/next-js-api-routes-how-to-read-files-from-directory-compatible-with-vercel-5fb5837694b9

    Here is the code that works in prod mode on vercel:

    export async function GET(request: Request, { params }: GetParams) {
      try {
        const filename = params.filename;
    
        if (!filename) {
          throw new Error('filename parameter should not be empty.');
        }
    
        let folderPath = '';
        if (filename.startsWith('myFile')) {
          folderPath = path.resolve('./public', 'myFiles');
        } else if (filename.startsWith('yourFile')) {
          folderPath = path.resolve('./public', 'yourFiles');
        } else if (filename.startsWith('theirFile')) {
          folderPath = path.resolve('./public', 'theirFiles');
        } else {
          throw new Error(`Unknown filename type: ${filename}`);
        }
    
        const files = await fs.promises.readdir(path.join(folderPath));
    
        for (const file of files) {
          if (file.startsWith(filename)) {
            const buffer = await readFile(path.join(folderPath, file));
            //Create headers
            const headers = new Headers();
            headers.append('content-disposition', `attachment; filename="${file}"`);
            headers.append('Content-Type', 'image/png');
    
            return new Response(buffer, {
              status: 200,
              headers,
            });
          }
        }
        return new Response(`File not found: ${filename}`, {
          status: 404,
        });
      } catch (error: any) {
        console.log(error);
        return new Response(`Internal server error`, {
          status: 500,
        });
      }
    }
    

    My understanding is: Don't use a composite path like

    'public/myFiles'

    rather use: path.resolve('./public', 'myFiles')

    Also, the following answer cleared my doubt about path.resolve vs path.join:

    What's the difference between path.resolve and path.join?

    I was using join rather than resolve in the first instance!