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 }); }); }; }; 
11
  • 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 Commented Nov 14, 2018 at 1: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: localhost:3000 Referer: 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 Commented Nov 14, 2018 at 1:56
  • No, I meant the field after this one. What is there after these headers? Commented Nov 14, 2018 at 1: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? Commented Nov 14, 2018 at 2: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" } Commented Nov 14, 2018 at 2:06

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.