Search code examples
typescriptnext.jsfile-uploadgoogle-cloud-storagesigned-url

File uploading to GCS using Signed URL from Nextjs - error code 403


The purpose is - -> to generate signed url in api/fileupload.js file to upload the file to GCS. -> Obtain the signed url from Nextjs server via nextjs API - localhost://3000/api/fileupload -> Uploading file to gcs using the generated signed url in index.jsx file

Signed URL is generated successfully. But while uploading the image body as form data to GCS, an error of 403 code is occured. Here is the response body.

body : (...)
bodyUsed : false
headers : 
Headers {}
ok : false
redirected : false
status : 0
statusText : ""
type : "opaque"
url : ""

Is the way to uploading file as formdata correct in index.jsx file? or what am I missing here?

The two files are given below -

index.jsx for nextjs file -

    
    import { useState } from "react";

    export default function Home() {
    const [url, setUrl] = useState("");
    const [file, setFile] = useState<any>(null);
    const [dataloaded, setDataloaded] = useState(true);
  
    const handleSubmit = async (e: any) => {
    setDataloaded(false);

    e.preventDefault();

    let formData = new FormData();
    formData.append("file", file.data);
    formData.append("Content-Type", `${file.data.type}`);

    console.log(file);
    const response = await fetch("http://localhost:3000/api/fileupload", {
      method: "POST",
      body: formData
    });
    const responseWithBody = await response.json();
    console.log(responseWithBody);
    setDataloaded(true);
    if (response.status === 200) {
      setUrl(responseWithBody.url);
    } else {
      console.log("error in generating url");
    }
    const response1 = await fetch(
      responseWithBody.url,

      {
        mode: "no-cors",
        method: "POST",
        body: formData,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "content-type": "image/png"
        }
      }
    );
    console.log(response1);
  };
const handleFileChange = (e: any) => {
      const img = {
          preview: URL.createObjectURL(e.target.files[0]),
          data: e.target.files[0]
       };
     setFile(img);
    };

  return (
     <>
      <div className="form-container">
        <form onSubmit={handleSubmit}>
          <div className="image-preview-container">
            {file ? (
              <img src={file.preview} alt="File to upload" />
            ) : (
              <img
            src="https://raw.githubusercontent.com/koehlersimon/fallback/master/Resources/Public/Images/placeholder.jpg"
             alt="Fallback"
           />
         )}
       </div>
       <div className="file-name">
         {file && file.data.name}
         {url && (
           <a href={url} target="_blank" rel="noreferrer">
             FiExternalLink
           </a>
         )}
       </div>
       <input
         type="file"
         name="file"
         onChange={handleFileChange}
         className="custom-file-input"
       ></input>
       <button
         className="submit-button"
         type="submit"
         disabled={!file}
         onClick={handleSubmit}
       >
         Submit
       </button>
     </form>
   </div>
 </>
 );
 }

fileupload.js in api/ folder -

import { Storage } from "@google-cloud/storage";
import multer from "multer";
import type { NextApiRequest, NextApiResponse } from "next";

const storage = new Storage({
  keyFilename: `service_account_key.json`,
  projectId: "my-project-id"
});
const bucketName = "my-bucket-name";

async function parseFormData(
  req: NextApiRequest & { files?: any },
  res: NextApiResponse
) {
  const storage = multer.memoryStorage();
  const multerUpload = multer({ storage });
  const multerFiles = multerUpload.any();
  await new Promise((resolve, reject) => {
    multerFiles(req as any, res as any, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
  return {
    fields: req.body,
    files: req.files
  };
}

export default async function handler(
  req: NextApiRequest & { files?: any },
  res: NextApiResponse<any>
) {
  const options = {
    version: "v4",
    action: "write",
    expires: Date.now() + 15 * 60 * 1000, // 15 minutes
    contentType: "application/octet-stream"
  } as any;

  const result = await parseFormData(req, res);
  // console.log(result);
  const file = storage
    .bucket(bucketName)
    .file(result?.files[0]?.originalname || "new-file.png");
  const [url]: any = await file.getSignedUrl(options);

  console.log("Generated PUT signed URL:");
  console.log(url);

  res.status(200).json({ url: url });
}



Solution

  • I wanted to send the name of the original file on which a signed URL will be generated and receive that signed URL using Nextjs GET API.

    Here is the solution code - in api/fileupload.ts

    import { Storage } from "@google-cloud/storage";
    import type { NextApiRequest, NextApiResponse } from "next";
    const storage = new Storage({
      keyFilename: `service_account_key.json`,
      projectId: "my-project-id"
    });
    const bucketName = "bucket-name";
    
    export default async function handler(
      req: NextApiRequest & { files?: any },
      res: NextApiResponse<any>
    ) {
      const options = {
        version: "v4",
        action: "write",
        expires: Date.now() + 15 * 60 * 1000 // 15 minutes
        // contentType: "application/octet-stream"
      } as any;
    
      const newFileName = req.query.name as string;
      const file = storage.bucket(bucketName).file(newFileName);
      const [url]: any = await file.getSignedUrl(options);
    
      console.log("Generated PUT signed URL:", url);
      res.status(200).json({ url: url });
    }
    
    

    Through Nextjs GET API signed URL is obtained and a PUT API is called with a signed URL, data saved from the target object of the event, and actual content type in the header.

    index.jsx file -

    import { useState } from "react";
    import axios from "axios";
    import Image from "next/image";
    import Link from "next/link";
    import { FiExternalLink } from "react-icons/fi";
    import Loader from "./Loader";
    export default function Home() {
      const [url, setUrl] = useState("");
      const [file, setFile] = useState<any>(null);
      const [dataloaded, setDataloaded] = useState(true);
      const [fileUploadDone, setFileUploadDone] = useState(false);
      
      
      const handleSubmit = async (e: any) => {
        setDataloaded(false);
        e.preventDefault();
        const response = await fetch(`/api/fileupload?name=${file.data.name}`, {
          method: "GET"
        });
        const responseWithBody = await response.json();
        console.log(responseWithBody.url);
    
        if (response.status === 200) {
          setUrl(responseWithBody.url);
        } else {
          console.log("error in generating url");
        }
        const response1 = await axios.put(responseWithBody.url, file.data, {
          headers: {
            "Content-Type": `${file.data.type}`
          }
        });
        if (response1.status === 200) {
          setFileUploadDone(true);
        } else {
        }
        setDataloaded(true);
        console.log(response1, file.data.type);
      };
      const handleFileChange = (e: any) => {
        const img = {
          preview: URL.createObjectURL(e.target.files[0]),
          data: e.target.files[0]
        };
        setFile(img);
      };
    
      return (
        <>
          <div className="form-container">
            <form onSubmit={handleSubmit}>
              <div className="image-preview-container">
                {file ? (
                  <Image
                    width={"400"}
                    height={"400"}
                    src={file.preview}
                    alt="File to upload"
                  />
                ) : (
                  <Image
                    width={"400"}
                    height={"400"}
                    src="https://raw.githubusercontent.com/koehlersimon/fallback/master/Resources/Public/Images/placeholder.jpg"
                    alt="Fallback"
                  />
                )}
              </div>
              <div className="file-name">
                {file && file.data.name}
               
              </div>
              <input
                type="file"
                name="file"
                onChange={handleFileChange}
                className="custom-file-input"
              ></input>
              <button
                className="submit-button"
                type="submit"
                disabled={!file}
                onClick={handleSubmit}
              >
                Submit
              </button>
              {fileUploadDone && (
                <span style={{ marginTop: "20px" }}>
                  File upload is done successfully.{" "}
                  <span
                    onClick={() => {
                      setFileUploadDone(false);
                      setFile(null);
                      setDataloaded(true);
                    }}
                  >
                    Click to Upload Again
                  </span>
                </span>
              )}
            </form>
          </div>
          {!dataloaded && <Loader />}
        </>
      );
    }