Express server posting twice, exactly two minutes apart? - node.js

I am trying to build a react chat box using an express server and pusher to listen for a dialogflow bot. It works fine at first, but the bot always responds a second time, repeating itself (but sometimes with a different response) exactly two minutes later to the second.
I have some logging statements in the server code to try and debug it, and have been monitoring the react front-end for network activity. It appears that react is only sending one fetch request, because there is only one network log in the browser. But on the server-side, the request is logged twice. I'm not sure why this is or what I'm doing wrong!
// server.js
require("dotenv").config({ path: "variables.env" });
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const processMessage = require("./process-message");
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.post("/chat", (req, res) => {
const { message } = req.body;
processMessage(message);
console.log(message);
});
app.set("port", process.env.PORT || 5000);
const server = app.listen(app.get("port"), () => {
console.log(`Express running → PORT ${server.address().port}`);
});
//process-message.js
const Dialogflow = require("dialogflow");
const Pusher = require("pusher");
const projectID = "firstchatbox-fakeURL";
const sessionID = "123456";
const languageCode = "en-US";
const config = {
credentials: {
private_key: process.env.DIALOGFLOW_PRIVATE_KEY,
client_email: process.env.DIALOGFLOW_CLIENT_EMAIL
}
};
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
encrypted: true
});
const sessionClient = new Dialogflow.SessionsClient(config);
const sessionPath = sessionClient.sessionPath(projectID, sessionID);
const processMessage = message => {
const request = {
session: sessionPath,
queryInput: {
text: {
text: message,
languageCode
}
}
};
sessionClient
.detectIntent(request)
.then(responses => {
console.log(responses);
const result = responses[0].queryResult;
return pusher.trigger("bot", "bot-response", {
message: result.fulfillmentText
});
})
.catch(err => {
console.error("ERROR:", err);
});
};
module.exports = processMessage;
// Here is the React front-end code, even though i'm ~60% sure
//the bug is server-side at this point
//App.js
import React, { Component } from "react";
import Pusher from "pusher-js";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
userMessage: "",
conversation: []
};
}
componentDidMount() {
const pusher = new Pusher("fakepusherappID454564564566", {
cluster: "us3"
});
const channel = pusher.subscribe("bot");
channel.bind("bot-response", data => {
const msg = {
text: data.message,
user: "ai"
};
this.setState({
conversation: [...this.state.conversation, msg]
});
});
}
handleChange = event => {
this.setState({ userMessage: event.target.value });
};
handleSubmit = event => {
event.preventDefault();
if (!this.state.userMessage.trim()) return;
const msg = {
text: this.state.userMessage,
user: "human"
};
this.setState({
conversation: [...this.state.conversation, msg]
});
fetch("http://localhost:5000/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: this.state.userMessage
})
})
.then(d => console.log(d))
.catch(e => console.log(e));
this.setState({ userMessage: "" });
};
render() {
const ChatBubble = (text, i, className) => {
return (
<div key={`${className}-${i}`} className={`${className} chat-bubble`}>
<span className="chat-content">{text}</span>
</div>
);
};
const chat = this.state.conversation.map((e, index) =>
ChatBubble(e.text, index, e.user)
);
return (
<div>
<h1>React Chatbot</h1>
<div className="chat-window">
<div className="conversation-view">{chat}</div>
<div className="message-box">
<form onSubmit={this.handleSubmit}>
<input
value={this.state.userMessage}
onInput={this.handleChange}
onChange={this.handleChange}
className="text-input"
type="text"
autoFocus
placeholder="Type your message and hit Enter to send"
/>
</form>
</div>
</div>
</div>
);
}
}
export default App;
It shows occasional console errors:
Source map error: TypeError: NetworkError when attempting to fetch resource.
Resource URL: http://localhost:3000/static/js/0.chunk.js
Source Map URL: 0.chunk.js.map
but I don't think they are relevant?

Exactly two minutes apart sounds like it could be a browser request that timed out and the browser is retrying.
And, now that I look at your app.post() handler, you don't seem to send any sort of response back to the client so that could cause an issue like this.
All http requests received by your server must send some sort of response, even if they just send a 404 or 500 status back. Either do res.sendStatus(200) or res.send("some response");.
You can open the debugger in Chrome and look at the network tab and then submit your form and watch exactly what network traffic happens between client and server.
And, since you tried this and it fixed your problem, I'm posting it as an answer.

Related

How to receive Cloudinary image URL immediately upon upload (React JS and Node Js)?

I can successfully upload images to Cloudinary. But my question is how can I get the Cloudinary url of the successfully uploaded image sent back to me immediately upon upload?
I know it's sent back as part of const uploadedResponse = await cloudinary.uploader.upload(fileStr, {upload_preset: 'dev_setups'}), but this is on the backend (see code #2 below), I would like to receive the URL on the frontend (see code #1 below) so I can set it to React state. What is the best approach to accomplishing this?
Please let me know if you need more details.
Code #1: Below is my code to upload a picture to Cloudinary (Cloudinary specific code is commented below for reference as /* Cloudinary upload */)
import React, { useState } from 'react'
import { Card, Button, CardContent } from '#material-ui/core';
import { post, makePostAction } from '../actions';
import { useSelector, useDispatch } from 'react-redux';
export default function MakePost() {
const [title, setTitle] = useState("")
const dispatch = useDispatch();
const usernameHandle = useSelector(state => state.username)
const [fileInputState, setFileInputState] = useState('') /* new */
const [previewSource, setPreviewSource] = useState('') /* new */
const [selectedFile, setSelectedFile] = useState('') /* new */
const onInputChange = (event) => {
setTitle(event.target.value);
}
const handleSubmit = (evt) => {
evt.preventDefault();
if (!title) return
dispatch(makePostAction({
title,
comment: false,
comments_text: "",
handle: usernameHandle,
post_date: new Date().getTime()
}))
setTitle("")
}
/* Cloudinary upload */
const handleFileInputChange = (e) => {
const file = e.target.files[0]
previewFile(file)
}
const previewFile = (file) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
setPreviewSource(reader.result)
}
}
const handleSubmitFile = (e) => {
e.preventDefault();
if(!previewSource) return;
uploadImage(previewSource)
}
const uploadImage = async (base64EncodedImage) => {
console.log(base64EncodedImage)
try {
await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({data: base64EncodedImage}),
headers: {'Content-type': 'application/json'}
})
} catch (error) {
console.error(error)
}
}
/* Cloudinary upload */
return (
<div>
<Card>
<CardContent>
<form onSubmit={handleSubmit}>
<input type="text" value={title} onChange={onInputChange} />
</form>
{/* new */}
<form onSubmit={handleSubmitFile} className="form">
<input type="file" name="image" onChange={handleFileInputChange} value={fileInputState} className="form-input" />
<button className="btn" type="submit">Submit</button>
</form>
{/* new */}
{previewSource && (
<img
src={previewSource}
alt="chosen"
style={{height: '300px'}}
/>
)}
</CardContent>
</Card>
</div>
)
}
Code #2: Here is my server.js
const express = require('express');
const app = express();
const {cloudinary} = require('./utils/cloudinary');
app.use(express.json({limit: '50mb'}));
app.use(express.urlencoded({limit: '50mb', extended: true}))
app.get('/api/images', async (req, res) => {
const {resources} = await cloudinary.search.expression('folder:dev_setups')
.sort_by('public_id', 'desc')
.max_results(1)
.execute()
const publicIds = resources.map(file => file.secure_url)
res.send(publicIds)
})
app.post('/api/upload', async (req, res) => {
try {
const fileStr = req.body.data;
const uploadedResponse = await cloudinary.uploader.upload(fileStr, {upload_preset: 'dev_setups'})
res.json({msg: "Success"})
} catch (error){
console.error(error)
res.status(500).json({err: 'Something went wrong'})
}
})
const port = process.env.PORT || 3001
app.listen(port, () => {
console.log(`listening on port ${port}`)
});
The Cloudinary upload response object includes a secure_url attribute which you can send back to the front end. Looking at code #2, it seems that you're currently sending a "Success" msg (res.json({msg: "Success"})). Sounds like you want to change that line to -
res.json({url: uploadedResponse.secure_url})
In your front end (code #1), I'd consider switching from async/await to .then mechanism, as you don't want to the entire app to wait for the response -
const uploadImage = (base64EncodedImage) => {
console.log(base64EncodedImage);
fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({data: base64EncodedImage}),
headers: {'Content-type': 'application/json'}
})
.then(doWhateverYouWant)
.catch((error) => console.error(error))
}
const doWhateverYouWant = async (res) => {
// you can use res.url
}

How do I debug server-side errors on MERN?

I have this front-end code:
export const CreatePage = () => {
const auth = useContext(AuthContext)
const {request} = useHttp()
const [content, setContent] = useState('')
const [title, setTitle] = useState('')
const [lead, setLead] = useState('')
useEffect(() => {
window.M.updateTextFields()
},[])
const postHandler = async () => {
try {
const data = await request('/api/post/generate', 'POST', {title: title, lead: lead, content: content}, {
Authorization: `Bearer ${auth.token}`
})
console.log(data)
} catch (e) {}
}
And this back-end code:
router.post('/generate', auth, async (req, res) => {
try {
const baseURL = config.get('baseURL')
const {title, lead, content} = req.body
// if (!title || !lead || !content) {
// return res.status(422).json({error: 'Please, input ALL fields'})
// }
const Post = new Post({
title, lead, content, owner: req.body.user.userId // req.user.userId
})
await Post.save()
res.status(201).json({Post})
} catch (e) {
res.status(500).json({message: 'Something went wrong'})
}})
I've tried a lot of things, but I still get this error. I know this is a server-side error, but that's all I have been able to figure out.
P.S. If there are any questions about the code, I will add it later.
UPD: By the way, could it be a problem's reason? Console log:
[1] Proxy error: Could not proxy request /api/post/generate from localhost:3000 to http://localhost:5000.
Probably, it's because of cors, you just can't send request from different url's. Try to install cors and configure it:
const cors = require("cors");
app.use("/", require('./src/routes'));
app.use(cors({
origin: '*'
}))

req.session with different domains node.js

I'm building an app using next.js and node.js (express).
The client run on localhost:4000, and the server run on lovalhost:3000.
to communicate between the two domains Iwm using cors().
to authorization i'm using with cookieSession and jwt.
app.use(
cookieSession({
signed: false,
secure: false,
sameSite: false,
})
);
when user login i put on the session a jwt.
req.session = {
jwt:userJwt
};
this time the user needs to be identified by the cookie.
this works nice in postman environment, when a user sign in he is identified, the cookie with his jwt is saved.
but when i try to make the same request via the client unforteantually the cookie is not saved.
the client code:
const signin= () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const {doRequest, errors} = useRequest({
url: 'http://localhost:4000/api/auth/signin',
method: 'post',
body: {
email,password
},
onSuccess: () => Router.push('/')
});
the useRequest hook:
import axios from 'axios';
import { useState } from 'react';
const useRequest= ({url, method, body, onSuccess}) => {
const [errors, setErrors] = useState(null);
const doRequest =async () => {
try {
setErrors(null);
console.log(url);
const response = await axios[method](url,body);
console.log(response);
if(onSuccess) {
onSuccess(response.data);
}
return response.data;
} catch (error) {
console.log(error);
setErrors(
<div className= "alert alert-danger" >
<h4>Ooops...</h4>
{error.response.data ?
<ul className="my-0">
{error.response.data.errors.map(err => (
<li key = {err.message}> {err.message}</li>
))}
</ul>:{}
}
</div>
);
}
};
return { doRequest,errors }
};
export default useRequest;
Maybe because the diffrent domains the session is not the same?
How can I slove it?
My app
I hope I was clearly.
thank you!
I'm using process.env variables

How can I retrieve search results with only one click of the search button instead of two?

I am working on a Node.js + ElasticSearch + React.js project and I have managed to get the search to work! However, I have to click the search button twice before I get back results in my console. Eventually, I would like to output the results via components to the user. any input would be great!
Here is React.js:
import React, { Component } from 'react';
import axios from 'axios';
class App extends Component {
state = {
result: [],
name: 'Roger',
userInput: null,
}
handleSubmit = event=> {
event.preventDefault();
var input = document.getElementById("userText").value;
this.setState({ userInput: input });
axios.get('http://localhost:4000/search?query=' + this.state.userInput)
.then(res => {
var result = res.data;
this.setState({ result: result });
console.log(this.state.result);
console.log(this.state.userInput);
})
}
render() {
return (
<div className="App">
<h2>hello from react</h2>
<form action="/search">
<input type="text" placeholder="Search..." name="query" id="userText"/>
<button type="submit" onClick={this.handleSubmit}><i>Search</i></button>
</form>
</div>
);
}
}
export default App;
here is Node.js:
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const JSON = require('circular-json');
const PORT = 4000;
var client = require ('./connection.js');
var argv = require('yargs').argv;
var getJSON = require('get-json');
const cors = require('cors');
let app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors({
origin: 'http://localhost:3001',
credentials: true
}));
app.get('/', function(req, res){
res.send("Node is running brother");
});
app.get("/search", function (request, response) {
client.search({
index: 'club',
type: 'clubinfo',
body: {
query: {
match: { "name": query}
},
}
},function (error, data, status) {
if (error) {
return console.log(error);
}
else {
// Send back the response
response.send(data);
}
});
});
app.listen(PORT, () => console.log('wowzers in me trousers, Listening on port ' + PORT));
this.setState() is an asynchronous function, which means that the updated data will only be available in its callback.
To better show this, try with this modification:
handleSubmit = event=> {
event.preventDefault();
var input = document.getElementById("userText").value;
axios.get('http://localhost:4000/search?query=' + input)
.then(res => {
var result = res.data;
this.setState({ result: result, userInput: input }, () => {
console.log(this.state.result);
console.log(this.state.userInput);
});
})
}
Note: you are handling your "userText" field as an uncontrolled field. This means that you let it be filled by native html+js, and just get the content from the DOM. Doing this, you never really need to have a "userInput" state variable.
Here is a snippet with userText as a controlled field:
class App extends Component {
state = {
result: [],
name: 'Roger',
userInput: '',
}
handleChange = event=> {
event.preventDefault();
this.setState({userInput: event.target.value});
}
handleSubmit = event=> {
event.preventDefault();
axios.get('http://localhost:4000/search?query=' + this.state.userInput)
.then(res => {
var result = res.data;
this.setState({ result: result });
console.log(this.state.result);
console.log(this.state.userInput);
})
}
render() {
return (
<div className="App">
<h2>hello from react</h2>
<form action="/search">
<input type="text" value={this.state.userInput} onChange={this.handleChange} placeholder="Search..." name="query" id="userText"/>
<button type="submit" onClick={this.handleSubmit}><i>Search</i></button>
</form>
</div>
);
}
}
I believe you'd better implement onChange function on your input, so you would have actual user search request the moment he or she would type it in.
Take official react doc for this example
https://reactjs.org/docs/forms.html
The reason it takes two times for the button to be pushed is that (may be) your state would become familiar with search request only after first push, and then you need second to actually get axios to send request (because it is null on first one)

Upload image file from React front end to Node/Express/Mongoose/MongoDB back end (not working)

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
});
});
};
};
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.

Resources