node.jsreactjsexpresscorsfetch

CORS issue when trying to upload a picture to image hosting service from react component


I'm working on a React app with Express JS, using CRA. I would like to upload a picture to image hosting service (such as imgur, imageshack or freeimage.host) as "simply" as possible, and get its URL back so that I can display it on my app. But I'm having a lot of trouble with CORS.

For the record, I have a basic general understanding of CORS, and how it prevents from doing request to a different domain. I've also seen many people have issues with CORS, and the gist of it seems to be that CORS must be handled by the backend server, and not the frontend. Which I don't understand very well.

For the record, here's essentially my test component (React) in which I'm trying to upload the picture.

import React, { useState, useContext } from 'react'
import { Form } from 'react-bootstrap'

export default function testUpload() {

    const [file, setFile] = useState(undefined);

    const handleEditForm = inputEvent => {
        const { files } = inputEvent.target;
    
        console.log(files[0]);
        setFile(files[0]);
    }
  
    const apiSendPic = async () => {
        
      const formData = new FormData()
      formData.append('file', file)
      formData.append("firstName", "Yamakyu")

      await fetch(
        'https://freeimage.host/api/1/upload?key=6d207e02198a847aa98d0a2a901485a5',
        {
          method: 'POST',
          body: formData,
        }
      )
      .then((response) => response.json())
      .then((result) => {
        console.log('Success:', result);
      })
      .catch((error) => {
        console.error('Error:', error);
      });
    }

    
    return (
        <div>
            <button onClick={apiSendPic}>Upload</button>

            <Form>
                <Form.Group controlId="fileName">
                    <Form.Label className='VerticalLabel'>Photo ou image :</Form.Label> <br />
                    <Form.Control
                        className='LargeInput'
                        type="file"
                        name='image'
                        onChange={handleEditForm}
                        size="lg" 
                    />
                </Form.Group>
            </Form>
        </div>
    )
}

And here's my server.js (I removed everything irrelevant to the question, such as all my other routes and such)

const path = require("path");
const express = require("express");
const app = express();
const db = require("./models");
const cors = require("cors");
const PORT = process.env.PORT || 8081;


const corsOptions = {
  origin: "*",
  credentials: true, //access-control-allow-credentials:true
  optionSuccessStatus: 200,
};
app.use(cors(corsOptions));

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static(path.join(__dirname, "./build_front")));

const RouteTest = require("./Routes/RouteTest");
app.use("/api/test", RouteTest);

db.sequelize.sync();

app.use("/api/reset", userController.isLoggedIn, (req, res) => {
  db.sequelize
    .sync({ force: true })
    .then(() => {
      console.log("DROP and re-sync db.");
    })
    .catch((err) => console.log(`Error while dropping/syncing db : ${err}`));

  return res.status(200).json({
    message: "Database droped",
    needLogout: false,
  });
});


app.get("*", (_, res) => res.sendFile("index.html", { root: "build" }));

app.listen(PORT, () => {
  console.log(`Listening to port ${PORT}.`);
});

One thing I don't understand very well is, if CORS needs to be handled by my backend server, does that mean the image upload should be done from my backend server ? And if so, how ? Because I tried to make a dedicated route with a controller to try to make even a simple request (no picture, just a single POST request with 1 key:value), but I could not get my controller to use fetch. It is apparently not recognized as a function. I did install the "node-fetch" package, as recommanded in many tutorials, but importing it seems to be difficult :

Here's a piece of my controller (the route works fine, I can call this api just fine, the api itself doesn't work).

const db = require("../models");
const fetch = import("node-fetch");

exports.upload = async (req, res) => {
  try {
    await fetch("https://freeimage.host/api/1/upload?key=6d207e02198a847aa98d0a2a901485a5", {
      method: "POST",
      headers: {
        "Content-type": "application/json",
      },
      body: JSON.stringify({
        name: "yamakyu",
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        console.log("API response ↓");
        console.log(data.message);
      });
  } catch (error) {
    console.log("↓ ------- ERROR ↓");
    console.log(error);
    return res.status(500).json({
      error,
    });
  }
};

I'm a bit at a loss. I feel like this shouldn't be complicated, but I feel stuck. I just want to be able to upload a picture, and get its URL.

Thanks in advance for your help


Solution

  • so I ended up solving the thing by myself. Responding to my own question so that other people who run into similar struggles can find a way out.

    I think my 2 biggest mistake was 1/ trying to stick with fetch when I could use axios just fine 2/ sticking with freeimage.host when other image hosting APIs exist.

    Things went a lot smoother when I tried to work with axios, and then a bit later when I gave up on freeimage.host for imgbb.com instead.

    Here's what I did. First, install axios and form-data using npm i axios and npm i form-data. And from there, I can import it using the require('axios') syntax I'm used to. You also want to request a API key. For imgbb.com you need to create an account and request a key here https://api.imgbb.com/

    And here's the important bits of code to make it work

    const fs = require("fs");
    const axios = require("axios");
    const formData = require("form-data");
    
    const uploadAxios = () => {
    
    try {
        console.log("ping !");
    
        const myFile = fs.readFileSync(
          `YOUR_FILE_LOCATION_ON_YOUR_COMPUTER`, { encoding: "base64" }
        );
    
        const myForm = new formData();
        myForm.append("image", myFile);
    
        await axios
          .post(
            `https://api.imgbb.com/1/upload?key=YOUR_KEY_YOU_GOT_ON_THE_API_PAGE`,
            myForm,
            {
              headers: {
                "Content-Type": "multipart/form-data",
              },
            }
          )
          .then((response) => {
            console.log("API response ↓");
            console.log(response);
          })
          .catch((err) => {
            console.log("API error ↓");
            console.log(err);
    
            if (err.response.data.error) {
              console.log(err.response.data.error);
              //When trouble shooting, simple informations about the error can be found in err.response.data.error so it's good to display it
            }
            
          });
      } catch (error) {
        console.log(error);
      }
    

    This the simplest I could make it work. There are probably other approaches, with better formatting or more precise options, but this works for me. Displaying the API response in the console will let you see that the image URL can be found in response.data.data.url.

    Of course, all this is done in one of my controllers in my Express server (since the original issue is CORS)

    I hope this'll be useful to some.