Introduction
I have researched this CORS problem. Chat GPT is going round in circles and nothing it has advised me to do has worked.
The functionality I've coded is quite simple, and you can even reproduce it to try for yourself: it's a simple canvas on which the user can draw a picture. The user then has the option of sending the drawing. It's on this last point that I'm stuck.
However, I think I'm doing the right thing: on the server side, I'm using the "cors" module and I'm clearly authorizing all origins (I know it's not safe, but I'm still trying).
Context
On the front-end, this is a React 18 application. On the back-end, I've built a NodesJS/Express API which is hosted in a VPS running on a Nginx web server.
The drawing is physically saved as a png in a folder on the server (initially only for viewing the image, but I could just as easily remove this part of the code) and in a CouchDB database in Base64: this is a conversion of the image into a very long character string that takes up very little space and which the browser itself can then reconstruct so that it's visible.
In my React component, I use fetch, which is a simple HTTPS POST request that saves the image in base64 in my database (called "usersdrawings").
Problem: when sending the drawing, I systematically get the following error message in my browser's console:
Cross-origin redirection to https://*********.fun/drawings/ denied by Cross-Origin >Resource Sharing policy: Origin http://localhost:3000 is not allowed by Access->Control-Allow-Origin. Status code: 301
So there are 3 important things to consider to solve the problem:
1-React component
To start with, here's my React component (the interesting thing is the handleSendDrawing function, which is triggered when the user clicks on the button to send his drawing):
import React, { useState, useRef, useEffect } from 'react';
import cursorImage from './cursor7.png';
import FalshBtn2 from '../FalshBtn2';
const DrawingCanvas = () => {
const canvasRef = useRef(null);
const [drawing, setDrawing] = useState(false);
const [currentColor, setCurrentColor] = useState('#000000');
const [canvasWidth, setCanvasWidth] = useState(740);
const [canvasHeight, setCanvasHeight] = useState(555);
useEffect(() => {
const interval = setInterval(() => {
let randomColor;
do {
randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
} while (randomColor === '#ffffff');
setCurrentColor(randomColor);
}, 500);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
context.lineWidth = 20;
context.lineCap = 'round';
const handleMouseDown = (event) => {
setDrawing(true);
drawPoint(event);
};
const handleMouseMove = (event) => {
if (!drawing) return;
drawPoint(event);
};
const handleMouseUp = () => {
setDrawing(false);
};
const drawPoint = (event) => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
context.fillStyle = currentColor;
context.beginPath();
context.arc(x, y, 15, 0, Math.PI * 2);
context.fill();
};
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
return () => {
canvas.removeEventListener('mousedown', handleMouseDown);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('mouseup', handleMouseUp);
};
}, [drawing, currentColor]);
const handleSendDrawing = async () => {
const canvas = canvasRef.current;
const dataURL = canvas.toDataURL();
const formData = new FormData();
formData.append('drawing', dataURL);
try {
const response = await fetch('https://************.fun/drawings', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Error');
}
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
} catch (error) {
console.error("Error", error);
}
};
return (
<>
<div className='lecadre'>
<div className='container_canvas_bouton'>
<div className='zoneArtistique'>
<canvas
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
className='dessiner'
style={{ cursor: `url(${cursorImage}) 10 10, auto` }}>
</canvas>
</div>
</div>
</div>
<div className="flex justify-center py-8">
<FalshBtn2 color={"cyan"} texte={"envoyer"} handleClick={handleSendDrawing} />
</div>
</>
);
};
export default DrawingCanvas;
2-NodeJS/Express API
On the back-end, here's the NodeJS/Express API script, which has a single route: /drawings:
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');
const logStream = fs.createWriteStream('logs.txt', { flags: 'a' });
const nano = require('nano');
const cors = require('cors');
require('dotenv').config();
// Middleware for debugging
app.use((req, res, next) => {
const logMessage = `[${new Date().toISOString()}] ${req.method} ${req.url}\n`;
logStream.write(logMessage + '/n');
next();
});
// Middleware CORS
app.use(cors({
origin: '*',
methods: ['OPTIONS', 'GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Middleware parse JSON
app.use(express.json());
// Middleware for parse datas POST
app.use(express.urlencoded({ extended: false }));
// error handle
app.use((err, req, res, next) => {
console.error(err.stack);
const errorMessage = `[${new Date().toISOString()}] An error has occurred: ${err.stack}\n`;
logStream.write(errorMessage + '\n');
res.status(500).json({ message: 'An error has occurred.' });
});
// couchDB access
const dbHost = process.env.DB_HOST_COUCHDB;
const dbUser = process.env.DB_USER_COUCHDB;
const dbPassword = process.env.DB_PSW_COUCHDB;
const dbPort = process.env.DB_PRT_COUCHDB;
// drawings folder
const drawingsDirectory = path.join(__dirname, 'drawings');
// couchDB connexion
const nanoOptions = {
url: `https://${dbUser}:${dbPassword}@${dbHost}:${dbPort}`,
parseUrl: false
};
const db = nano(nanoOptions);
//couchDB database
const drawingsDB = db.use('usersdrawings');
// Endpoint
app.post('/drawings', async (req, res) => {
const dataURL = req.body.drawing;
// Vérify folder space
const directorySize = getDirectorySize(drawingsDirectory);
const maxDirectorySize = 2 * 1024 * 1024 * 1024; // 2 Go max
if (directorySize >= maxDirectorySize) {
return res.status(500).json({ error: 'Space of storage is full' });
}
// Create file png of drawing
const fileName = `drawing_${Date.now()}.png`;
const filePath = path.join(drawingsDirectory, fileName);
fs.writeFile(filePath, dataURL.replace(/^data:image\/png;base64,/, ''), 'base64', async (err) => {
if (err) {
logStream.write('Error :' + err + '\n');
return res.status(500).json({ error: 'Error' });
}
try {
// save drawing in base64 in CouchDB database
await drawingsDB.insert({
drawing: dataURL,
filePath: filePath
});
res.status(200).json({ message: 'Success !' });
logStream.write('Drawing saved in couchDB !' + '\n');
} catch (error) {
logStream.write('Error during save process in couchDB' + error + '\n');
res.status(500).json({ error: 'Error during save process in couchDB' });
}
});
});
// Function verify the size
function getDirectorySize(directory) {
let size = 0;
const files = fs.readdirSync(directory);
files.forEach(file =>{
const filePath = path.join(directory, file);
const stats = fs.statSync(filePath);
size += stats.size;
});
logStream.write('Total folder size: ' + size + '\n');
return size;
}
app.listen(3002, () => {
logStream.write(`API in port 3002` + '\n');
console.log('API running on port 3002');
});
3 - The service that makes the API accessible is active:
● my-api-usersdrawings.service - users drawings API
Loaded: loaded (/etc/systemd/system/my-api-usersdrawings.service; enabled; vendor >preset: enabled)
Active: active (running) since Sun 2024-03-24 04:51:21 CET; 2s ago
Main PID: 3297242 (node)
Tasks: 11 (limit: 2309)
Memory: 16.8M
CPU: 177ms
CGroup: /system.slice/my-api-usersdrawings.service
└─3297242 /usr/bin/node /opt/api-drawings/app.js
Mar 24 04:51:21 ubuntu-s-1vcpu-2gb-intel-fra1-01 systemd[1]: Started users drawings API
Mar 24 04:51:21 ubuntu-s-1vcpu-2gb-intel-fra1-01 node[3297242]: API running on port 3002
4- Finally, here's the Nginx configuration file for the domain name (which has a valid SSL certificate).
# domain name used to access couchDB in HTTPS
server {
listen 80;
listen [::]:80;
server_name *******************.fun;
location /.well-known/acme-challenge/ {
root /var/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name *******************.fun;
ssl_certificate /etc/letsencrypt/live/*******************.fun/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/*******************.fun/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# NodeJS/Express for users drawings running on port 3002
location /drawings/ {
proxy_pass http://localhost:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Access-Control-Allow-Origin *;
}
# other NodeJS API running on port 3000 used by another website
location /psychology/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Access-Control-Allow-Origin *;
}
# other NodeJS API running on port 3001 used by another website
location /novels/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Access-Control-Allow-Origin *;
}
}
What can I try next?