1

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 });
}


Nickname
  • 65
  • 1
  • 8
  • Form data is not an octet-stream. Form data uploads use POST and not PUT. – John Hanley May 29 '23 at 07:37
  • Have a look at this stackoverflow [link1](https://stackoverflow.com/questions/59879804/getting-403-while-trying-to-upload-to-gcp-signed-url-that-was-generated-with-mul),[link2](https://stackoverflow.com/questions/55105537/getting-403-forbidden-when-generating-signed-url-with-put-request-to-upload-an-o) & github [thread](https://github.com/googleapis/google-cloud-node/issues/1695) – Sathi Aiswarya May 29 '23 at 11:50
  • @SathiAiswarya, Thanks for link 1. I believe there is no issue in generating the signed URL, I have a similar project of Reactjs and Nodejs, generated a signed URL in the backend, and uploaded the file from the backend to GCS. I have compared the signed URL generated from both projects, they are identical. I think the problem is within the part of file uploading using signed URL from Nextjs file. – Nickname May 30 '23 at 03:39
  • @JohnHanley, I have changed to POST, facing same issue – Nickname May 30 '23 at 03:43
  • Review how to use a Signed URL for uploads It must be an HTTP PUT and must be an octet-stream. If you want to use a form upload then you must use a policy document. https://cloud.google.com/storage/docs/xml-api/post-object-forms. – John Hanley May 30 '23 at 04:04
  • Thanks, everyone. The Problems are solved. The problem was - 1. While generating a signed URL, in the options object, the content type of the octet form wasn't necessary for the api/fileupload. 2. while uploading the image to the designated signed URL, formData wasn't needed, simply the object as form.data format was enough to upload. – Nickname May 30 '23 at 09:13
  • Can you post your answer as a solution? So that, it will be useful for others who are facing same issue – Sathi Aiswarya May 30 '23 at 12:50

1 Answers1

1

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 />}
    </>
  );
}

Nickname
  • 65
  • 1
  • 8