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 }); }); }; };
Request Headers). Please paste that part hereconfigvariable? 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?