'multer file upload req.file.path undefined put request

I've created a simple RESTapi with express and for file upload I'm using multer. Creating a new item / post works. My issue is with the put route and the function to handle saving and updating the post/gear.

When the product image field is left blank, it returns

undefined [Object: null prototype] {
  title: 'joey',
  price: '123',
  weight: '123',
  description: 'lorem'
}
(node:5293) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'path' of undefined
    at /Users/Zack/Desktop/LeFIAT/lefiatV2/routes/gear.js:81:34

When all the fields in the product fields are filled out returns a duplicate key

MongoServerError: E11000 duplicate key error collection: lefiatv2.products index: slug_1 dup key: { slug: "joey" }
    at /Users/Zack/Desktop/LeFIAT/lefiatV2/node_modules/mongodb/lib/operations/update.js:80:33

The object is being populated I just can't seem to find the correct path or set up for the function

{
  fieldname: 'image',
  originalname: 'TorridV2_Toggle__.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: './public/products/',
  filename: '2022-02-02T15:44:30.813ZTorridV2_Toggle__.jpg',
  path: 'public/products/2022-02-02T15:44:30.813ZTorridV2_Toggle__.jpg',
  size: 23382
} [Object: null prototype] {
  title: 'joey',
  price: '123',
  weight: '123',
  description: 'lorem'
}

edit.ejs

Edit Product
<form action="/gear/<%= product.slug %>?_method=PUT" method="POST" enctype="multipart/form-data">
    <%- include('../partials/product_fields')  %>
</form>

product_fields.ejs

<div class="form-group">
    <label for="title">Title</label>
    <input type="text" name="title" id="title" class="form-control" value="<%= product.title %>" required>
</div>

<div class="form-group">
    <label for="price">Price</label>
    <input type="number" min="1" step="0.01" name="price" id="price" class="form-control"
        required><%= product.price %></input>
</div>

<div class="form-group">
    <label for="weight">Weight</label>
    <input type="number" min="1" step="0.01" name="weight" id="weight" class="form-control"
        required><%= product.weight %></input>
</div>

<div class="form-group">
    <label for="description">Description</label>
    <textarea name="description" id="description" class="form-control" required><%= product.description %></textarea>
</div>

<div class="form-group">
    <label for="image">Upload Images</label>
    <input type="file" name="image" id="image" class="form-control-file" multiple>
</div>


<a href="/gear" class="btn btn-secondary"> Cancel </a>

<button type="submit" class="btn btn-primary"> Save </button>

product.js modal

const { date } = require('joi')
const mongoose = require('mongoose')
const slugify = require('slugify')

const productSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    description: {
        type: String,
        required: true,
    },
    price: {
        type: Number,
        required: true
    },
    weight: {
        type: Number,
        required: true,
    },
    slug: {
        type: String,
        required: true,
        unique: true
    },
    createdAt: {
        type: Date,
        default:Date.now
    },
    image: {
        type: String,
        data: Buffer
    }
})

productSchema.pre('validate', function(next) {
    if (this.title) {
        this.slug = slugify(this.title, { lower: true, strict: true })
    }

    next()

})

module.exports = mongoose.model('Product', productSchema)

gear.js route

const express = require('express')
const multer = require('multer')
const Product = require('./../models/product')
const router = express.Router()

const storage = multer.diskStorage({

    // destination for file
    destination: function (req, file, callback) {
        callback(null, './public/products/')
    },
    // add back the extension
    filename: function (req, file, callback) {
        callback(null, new Date().toISOString() + file.originalname)
    },
})

const fileFilter = (req, file, cb) => {
    // reject a file
    if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
      cb(null, true);
    } else {
      cb(null, false);
    }
  };

  // upload parameters for multer 

  const upload = multer({
    storage: storage,
    limits: {
        fileSize: 1024*1024*3,
    },
    fileFilter: fileFilter
})

router.get('/', async (req, res) => {
    const products = await Product.find().sort({ createdAt: 'desc' })
    res.render('gear/index', { products: products })
})

router.get('/new', (req, res) => {
    res.render('gear/new', { product: new Product() })
})

router.get('/edit/:id', async (req, res) => {
    const product = await Product.findById(req.params.id)
    res.render('gear/edit', { product: product })
})

router.get('/:slug', async (req, res) => {
    const product = await Product.findOne({ slug: req.params.slug })
    if(product == null) res.redirect('/gear')
    else res.render('gear/show', { product: product })
})

router.post('/', upload.single('image'), async (req, res, next) => {
    req.product = new Product()  
    next()
}, saveProductAndRedirect('new'))

router.put('/:id', upload.single('image'), async (req, res, next) => {
    console.log(req.file, req.body)
    req.product = await Product.findOneAndUpdate({ slug: req.params.slug })
    next()
}, saveProductAndRedirect(`edit`))

router.delete('/:id', async (req, res) => {
    await Product.findByIdAndDelete(req.params.id)
    res.redirect(`/gear`)
})

function saveProductAndRedirect(path) {  
    
    return async (req, res) => {
        let product = req.product
        product.title = req.body.title
        product.description = req.body.description
        product.price = req.body.price
        product.weight = req.body.weight
        product.image = req.file.path
        
        try {
            if (! req.file || ! req.file.path) {
                return res.sendStatus(400);
              }
            product = await product.save()
            res.redirect(`/gear/${product.slug}`)
        } catch(err) {  
            console.log(err)
            res.render(`gear/${path}`, { product: product })
        }      
        }
    }

module.exports = router

server.js

if (process.env.NODE_ENV !== 'production') {
    require('dotenv').config()
}

const express = require('express')
const path = require('path');
const ejsMate = require('ejs-mate');
const session = require('express-session');
const methodOverride = require('method-override');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const mongoSanitize = require('express-mongo-sanitize');
const expressLayouts = require('express-ejs-layouts')
const ExpressError = require('./utils/ExpressError');


const indexRouter = require('./routes/index')
const tripsRouter = require('./routes/trips')
const gearRouter = require('./routes/gear')
const aboutRouter = require('./routes/about')
const blogRouter = require('./routes/blog')

const app = express()

app.engine('ejs', ejsMate)
app.set('view engine', 'ejs')
app.set('views', __dirname + '/views')

app.use('/public', express.static(__dirname + '/public'));
app.use(express.urlencoded({ extended: false }));
app.use(methodOverride('_method'));
app.set('layout', 'layouts/layout')
app.use(expressLayouts)
app.use(express.static(path.join(__dirname, 'public')))
app.use(mongoSanitize({
    replaceWith: '_'
}))

const MongoDBStore = require('connect-mongo')(session)

const mongoose = require('mongoose')
mongoose.connect(process.env.DATABASE_URL, {
    useNewUrlParser: true })
    const db = mongoose.connection
    db.on('error', error => console.error(error))
    db.once('open', () => console.log('Connected to Mongoose'))

    const secret = process.env.SECRET || 'thisshouldbeabettersecret!';
    const dbUrl = process.env.DATABASE_URL || 'mongodb://localhost:3000/lefiatv2'

    const store = new MongoDBStore({
        url: dbUrl,
        secret,
        touchAfter: 24 * 60 * 60
    });
    
    store.on('error', function (e) {
        console.log('session store error', e)
    })
    
    const sessionConfig = {
        store,
        name: 'session',
        secret,
        resave: false,
        saveUninitialized: true,
        cookie: {
            httpOnly: true,
            secure: true,
            expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
            maxAge: 1000 * 60 * 60 * 24 * 7
        }
    }


    app.use(session(sessionConfig));

    app.use(passport.initialize());
    app.use(passport.session());

app.use('/', indexRouter)
app.use('/trips', tripsRouter)
app.use('/gear', gearRouter)
app.use('/about', aboutRouter)
app.use('/blog', blogRouter)

  app.use((err, req, res, next) => {
    const { statusCode = 500 } = err;
    if (!err.message) err.message = 'Oh No, Something Went Wrong!'
    res.status(statusCode).render('error', { err })
})

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Serving on port ${port}`)
})


Solution 1:[1]

use optional chaining on your req.file.path so it would be like this

req.file?.path (kinda)

the error is telling you - hey where is the path?, there is no path in undefined - so your result if not passing the file will be

undefined.path

here is some example (i use nestjs):

async updateCommit(
@Res() res,
@Param('user_id') user_id: string,
@Param('repo_id') repo_id: string,
@Param('commited_id') commited_id: string,
@Body() updatedCommit: UpdateCommitDto,
@UploadedFile() file: UpdateCommitDto["file"]): Promise<User> {
Object.assign(updatedCommit, {
  file: file?.path,
  createdAt: false,
  updatedAt: Date.now
})
const response = await this.commitService.updateCommit(user_id, repo_id, commited_id, updatedCommit)
return res.status(HttpStatus.OK).json({ response })}

The result :

Before update

After update

sorry for the image ( low reputation lol )

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1