2

I’ve spent most of a day looking into this and trying to make it work. This is an app with a React/Redux front end, and a Node/Express/Mongoose/MongoDB back end.

I currently have a Topics system where an authorized user can follow/unfollow topics, and an admin can Add/Remove topics. I want to be able to upload an image file when submitting a new topic, and I want to use Cloudinary to store the image and then save the images path to the DB with the topic name.

The problem I am having is that I am unable to receive the uploaded file on the back end from the front end. I end up receiving an empty object, despite tons of research and trial/error. I haven’t finished setting up Cloudinary file upload, but I need to receive the file on the back end before even worrying about that.

SERVER SIDE index.js:

const express = require("express");
const http = require("http");
const bodyParser = require("body-parser");
const morgan = require("morgan");
const app = express();
const router = require("./router");
const mongoose = require("mongoose");
const cors = require("cors");
const fileUpload = require("express-fileupload");
const config = require("./config");

const multer = require("multer");
const cloudinary = require("cloudinary");
const cloudinaryStorage = require("multer-storage-cloudinary");

app.use(fileUpload());

//file storage setup
cloudinary.config({
  cloud_name: "niksauce",
  api_key: config.cloudinaryAPIKey,
  api_secret: config.cloudinaryAPISecret
});

const storage = cloudinaryStorage({
  cloudinary: cloudinary,
  folder: "images",
  allowedFormats: ["jpg", "png"],
  transformation: [{ width: 500, height: 500, crop: "limit" }] //optional, from a demo
});

const parser = multer({ storage: storage });

//DB setup
mongoose.Promise = global.Promise;
mongoose.connect(
  `mongodb://path/to/mlab`,
  { useNewUrlParser: true }
);

mongoose.connection
  .once("open", () => console.log("Connected to MongoLab instance."))
  .on("error", error => console.log("Error connecting to MongoLab:", error));

//App setup
app.use(morgan("combined"));
app.use(bodyParser.json({ type: "*/*" }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
router(app, parser);

//Server setup
const port = process.env.PORT || 3090;
const server = http.createServer(app);
server.listen(port);
console.log("server listening on port: ", port);

TopicController/CreateTopic

exports.createTopic = function(req, res, next) {
  console.log("REQUEST: ", req.body); //{ name: 'Topic with Image', image: {} }
  console.log("IMAGE FILE MAYBE? ", req.file); //undefined
  console.log("IMAGE FILES MAYBE? ", req.files); //undefined

  const topic = new Topic(req.body);
  if (req.file) {
    topic.image.url = req.file.url;
    topic.image.id = req.file.publid_id;
  } else {
    console.log("NO FILE UPLOADED");
  }

  topic.save().then(result => {
    res.status(201).send(topic);
  });
};

router.js

module.exports = function(app, parser) {
  //User
  app.post("/signin", requireSignin, Authentication.signin);
  app.post("/signup", Authentication.signup);
  //Topic
  app.get("/topics", Topic.fetchTopics);
  app.post("/topics/newTopic", parser.single("image"), Topic.createTopic);
  app.post("/topics/removeTopic", Topic.removeTopic);
  app.post("/topics/followTopic", Topic.followTopic);
  app.post("/topics/unfollowTopic", Topic.unfollowTopic);
};

CLIENT SIDE

Topics.js:

import React, { Component } from "react";
import { connect } from "react-redux";
import { Loader, Grid, Button, Icon, Form } from "semantic-ui-react";

import {
  fetchTopics,
  followTopic,
  unfollowTopic,
  createTopic,
  removeTopic
} from "../actions";

import requireAuth from "./hoc/requireAuth";

import Background1 from "../assets/images/summer.jpg";
import Background2 from "../assets/images/winter.jpg";

const compare = (arr1, arr2) => {
  let inBoth = [];
  arr1.forEach(e1 =>
    arr2.forEach(e2 => {
      if (e1 === e2) {
        inBoth.push(e1);
      }
    })
  );
  return inBoth;
};

class Topics extends Component {
  constructor(props) {
    super(props);

    this.props.fetchTopics();
    this.state = {
      newTopic: "",
      selectedFile: null,
      error: ""
    };
  }

  onFollowClick = topicId => {
    const { id } = this.props.user;

    this.props.followTopic(id, topicId);
  };

  onUnfollowClick = topicId => {
    const { id } = this.props.user;

    this.props.unfollowTopic(id, topicId);
  };

  handleSelectedFile = e => {
    console.log(e.target.files[0]);
    this.setState({
      selectedFile: e.target.files[0]
    });
  };

  createTopicSubmit = e => {
    e.preventDefault();
    const { newTopic, selectedFile } = this.state;
    this.props.createTopic(newTopic.trim(), selectedFile);

    this.setState({
      newTopic: "",
      selectedFile: null
    });
  };

  removeTopicSubmit = topicId => {
    this.props.removeTopic(topicId);
  };

  renderTopics = () => {
    const { topics, user } = this.props;

    const followedTopics =
      topics &&
      user &&
      compare(topics.map(topic => topic._id), user.followedTopics);

    console.log(topics);

    return topics.map((topic, i) => {
      return (
        <Grid.Column className="topic-container" key={topic._id}>
          <div
            className="topic-image"
            style={{
              background:
                i % 2 === 0 ? `url(${Background1})` : `url(${Background2})`,
              backgroundRepeat: "no-repeat",
              backgroundPosition: "center",
              backgroundSize: "cover"
            }}
          />
          <p className="topic-name">{topic.name}</p>
          <div className="topic-follow-btn">
            {followedTopics.includes(topic._id) ? (
              <Button
                icon
                color="olive"
                onClick={() => this.onUnfollowClick(topic._id)}
              >
                Unfollow
                <Icon color="red" name="heart" />
              </Button>
            ) : (
              <Button
                icon
                color="teal"
                onClick={() => this.onFollowClick(topic._id)}
              >
                Follow
                <Icon color="red" name="heart outline" />
              </Button>
            )}
            {/* Should put a warning safety catch on initial click, as to not accidentally delete an important topic */}
            {user.isAdmin ? (
              <Button
                icon
                color="red"
                onClick={() => this.removeTopicSubmit(topic._id)}
              >
                <Icon color="black" name="trash" />
              </Button>
            ) : null}
          </div>
        </Grid.Column>
      );
    });
  };

  render() {
    const { loading, user } = this.props;

    if (loading) {
      return (
        <Loader active inline="centered">
          Loading
        </Loader>
      );
    }

    return (
      <div>
        <h1>Topics</h1>
        {user && user.isAdmin ? (
          <div>
            <h3>Create a New Topic</h3>
            <Form
              onSubmit={this.createTopicSubmit}
              encType="multipart/form-data"
            >
              <Form.Field>
                <input
                  value={this.state.newTopic}
                  onChange={e => this.setState({ newTopic: e.target.value })}
                  placeholder="Create New Topic"
                />
              </Form.Field>
              <Form.Field>
                <label>Upload an Image</label>
                <input
                  type="file"
                  name="image"
                  onChange={this.handleSelectedFile}
                />
              </Form.Field>
              <Button type="submit">Create Topic</Button>
            </Form>
          </div>
        ) : null}

        <Grid centered>{this.renderTopics()}</Grid>
      </div>
    );
  }
}

const mapStateToProps = state => {
  const { loading, topics } = state.topics;
  const { user } = state.auth;

  return { loading, topics, user };
};

export default requireAuth(
  connect(
    mapStateToProps,
    { fetchTopics, followTopic, unfollowTopic, createTopic, removeTopic }
  )(Topics)
);

TopicActions/createTopic:

export const createTopic = (topicName, imageFile) => {
  console.log("IMAGE IN ACTIONS: ", imageFile); //this is still here 
  // const data = new FormData();
  // data.append("image", imageFile);
  // data.append("name", topicName);

  const data = {
    image: imageFile,
    name: topicName
  };
  console.log("DATA TO SEND: ", data); //still shows image file 
  return dispatch => {
    // const config = { headers: { "Content-Type": "multipart/form-data" } };
        // ^ this fixes nothing, only makes the problem worse 

    axios.post(CREATE_NEW_TOPIC, data).then(res => {
      dispatch({
        type: CREATE_TOPIC,
        payload: res.data
      });
    });
  };
};

When I send it like this, I receive the following on the back end: (these are server console.logs) REQUEST: { image: {}, name: 'NEW TOPIC' } IMAGE FILE MAYBE? undefined IMAGE FILES MAYBE? undefined NO FILE UPLOADED

If I go the new FormData() route, FormData is an empty object, and I get this server error: POST http://localhost:3090/topics/newTopic net::ERR_EMPTY_RESPONSE

export const createTopic = (topicName, imageFile) => {
  console.log("IMAGE IN ACTIONS: ", imageFile);
  const data = new FormData();

  data.append("image", imageFile);
  data.append("name", topicName);

  // const data = {
  //   image: imageFile,
  //   name: topicName
  // };
  console.log("DATA TO SEND: ", data); // shows FormData {} (empty object, nothing in it)
  return dispatch => {
    // const config = { headers: { "Content-Type": "multipart/form-data" } };
    // ^ this fixes nothing, only makes the problem worse

    axios.post(CREATE_NEW_TOPIC, data).then(res => {
      dispatch({
        type: CREATE_TOPIC,
        payload: res.data
      });
    });
  };
};
Nik Hammer-Ellis
  • 1,042
  • 2
  • 9
  • 14
  • Open your devtools, network tab and see if the multipart data is available in the latter part of the request (after `Request Headers`). Please paste that part here – Masious Nov 14 '18 at 01:31
  • Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9 Connection: keep-alive Content-Length: 37 Content-Type: application/json;charset=UTF-8 Host: localhost:3090 Origin: http://localhost:3000 Referer: http://localhost:3000/topics User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36 – Nik Hammer-Ellis Nov 14 '18 at 01:56
  • No, I meant the field after this one. What is there after these headers? – Masious Nov 14 '18 at 01:57
  • *" ..this fixes nothing, .."* So where exactly did you ever use the `config` variable? I see nothing in the code or even a commented attempt that shows you using it. See [How do I set multipart in axios with react?](https://stackoverflow.com/a/42096508/2313887) – Neil Lunn Nov 14 '18 at 02:01
  • Good link, will try that pattern. Ya I had that config variable in the axios post, just deleted instead of commenting out. @Masious just the request payload, as follows: { image: {}, name: "name I submitted" } – Nik Hammer-Ellis Nov 14 '18 at 02:06
  • As you can see, your image is not uploaded to the server as the `image` key is an empty JSON object. Now try it again and paste the data with the `config` variable uncommented and used in `Axios.post` – Masious Nov 14 '18 at 02:08
  • Any ideas on resolving this? Access to XMLHttpRequest at 'http://localhost:3090/topics/newTopic' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. – Nik Hammer-Ellis Nov 14 '18 at 02:11
  • The header causes that error – Nik Hammer-Ellis Nov 14 '18 at 02:13
  • You need to enable CORS on the server. Just add this: `app.use(cors())` in your server-side `index.js` after running `npm install cors` on the server root. – Masious Nov 14 '18 at 02:13
  • I already have that implemented, recheck my server index above. – Nik Hammer-Ellis Nov 14 '18 at 02:14
  • Specifically I get this now Error: Multipart: Boundary not found – Nik Hammer-Ellis Nov 14 '18 at 03:12

1 Answers1

0

Solution was to switch to using Firebase instead, and deal with image upload on the React client (this was attempted with cloudinary but with no success). The resulting download url can be saved to the database with the topic name (which is all I wanted from cloudinary) and now it is displaying the correct images along with the topics.

Nik Hammer-Ellis
  • 1,042
  • 2
  • 9
  • 14