Search code examples
javascriptnode.jsajaxformspost

How do I set up my POST form submission to stay on the same page and input any errors in a div?


SOLUTION FOUND: SEE BOTTOM FOR UPDATED CODE

I have been trying to get this to work for about 8 hours now. I cannot for the life of me get a script to work for this. I have tried many different scripts and I am going crazy. See attached my server.js file which works completely without any issues on its own, however it sends the end user of my site to a new page (/send-email) to send the POST and show the submission confirmation/error messages. I want it to all happen without leaving the page that the form is on. Trying to get that to work is killing me. I've tried looking through old forums and even tried for a while with ChatGPT, Llama etc but AI is worse at JS than I am apparently LOL. Please help me if you can. I'm losing my mind.

Server.js: (running on a Node.js/NGinx server)


const express = require('express');
const nodemailer = require('nodemailer');
const path = require('path');
const { body, validationResult } = require('express-validator'); // **Added for validation**
require('dotenv').config(); // Load environment variables from .env file
const rateLimit = require('express-rate-limit'); // Added for rate limiting

const app = express();
const port = process.env.PORT; // Use environment variable

//see end-user IP rather than cloudfare IP when limiting form submissions by IP
app.set('trust proxy', true);

// Serve static files (like your HTML form)
app.use(express.static(path.join(\__dirname, 'public')));

// Middleware to parse form data
app.use(express.urlencoded({ extended: true }));

// Configure rate limiter
const formSubmitLimiter = rateLimit({
windowMs: 24 \* 60 \* 60 \* 1000, // 24 hours
max: 3, // Limit each IP to 3 requests per `windowMs`
standardHeaders: true, // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
message: 'Too many form submissions from this device, please try again later.',
});

// Route to handle form submission
app.post('/send-email',

     // Honeypot check
     (req, res, next) => {
        if (req.body.body2) {
            return res.status(400).send('Form submission failed.');
        }
        next();
    },
    
    // Apply rate limiter to the route
    formSubmitLimiter,
    
    // validation middleware
    [
      body('subject').isLength({ min: 1 }).withMessage('Subject is required'),
      body('body').isLength({ min: 1 }).withMessage('Message body is required')
    ],
    
    async (req, res) => {
      const errors = validationResult(req); //Check validation errors
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
      
      const { subject, body } = req.body;
    
    
      // Configure nodemailer transport for SMTP service
      let transporter = nodemailer.createTransport({
        service: process.env.SMTP_SERVICE,
        auth: {
          user: process.env.EMAIL_USER,  // Use environment variable
          pass: process.env.EMAIL_PASS   // Use environment variable
        }
      });

try {
// Send email
let info = await transporter.sendMail({
from: `"SITENAMEREDACTED" <${process.env.EMAIL_USER}>`,  // Use environment variable
to: process.env.RECIPIENT_EMAIL,                // Use environment variable
subject: subject,
text: body,
});

    console.log('Message sent: %s', info.messageId);
    res.status(200).send('Email sent successfully!');

} catch (error) {
console.error('Error sending email:', error);
res.status(500).send('Error sending email');
}
});

// Start server
app.listen(port, () =\> {
console.log(`Server listening on http://localhost:${port}`);
});

HTML form:

<form id="contactForm" action="/send-email" method="POST" style="z-index: 200;">
  <div>
    <input autocomplete="off" type="text" placeholder="Subject" name="subject" required />
  </div>
  <div>
    <textarea autocomplete="off" placeholder="Your message" name="body" required></textarea>
  </div>

                            <div class="oh-no-hun" >
                              <input tabindex="-1" autocomplete="off" type="text" name="body2" />
                            </div>
  <div>
    <button type="submit">Send a message</button>
  </div>
</form>

<div id="message" class="message"></div>

One of my various troubled scripts:

<script>
  document.getElementById('contactForm').addEventListener('submit', async function(event) {
    event.preventDefault(); // Prevent the default form submission

    const form = event.target;
    const formData = new FormData(form);
    const messageDiv = document.getElementById('message');

    // Manually construct the request payload
    const payload = {
      subject: formData.get('subject'),
      body: formData.get('body'),
    };

    try {
      const response = await fetch(form.action, {
        method: form.method,
        headers: {
          'Content-Type': 'application/json', // Specify JSON content type
        },
        body: JSON.stringify(payload), // Convert payload to JSON
      });

      // Ensure response is in JSON format and read it
      const contentType = response.headers.get('content-type');
      let responseJson = {};

      if (contentType && contentType.includes('application/json')) {
        try {
          responseJson = await response.json();
        } catch (jsonError) {
          console.error('Failed to parse JSON:', jsonError);
          messageDiv.innerHTML = `<p class="error">Failed to parse server response.</p>`;
          return;
        }
      } else {
        const responseText = await response.text(); // Read as text if not JSON
        console.error('Unexpected response format:', responseText);
        messageDiv.innerHTML = `<p class="error">Unexpected response format. Response: ${responseText}</p>`;
        return;
      }

      if (response.ok) {
        // Display success message
        messageDiv.innerHTML = `<p class="success">${responseJson.message || 'Email sent successfully!'}</p>`;
      } else {
        // Handle and display validation errors
        const errors = responseJson.errors || [{ msg: 'An unknown error occurred.' }];
        messageDiv.innerHTML = `<p class="error">${errors.map(err => err.msg).join(', ')}</p>`;
      }

    } catch (error) {
      messageDiv.innerHTML = `<p class="error">An unexpected error occurred: ${error.message}</p>`;
    }
  });
</script>

I really just need to know what direction to go, starting from scratch with the script. If you can just point me in the right directions for writing the script I would love you. If you write it so it works, I would die from joy.

I have tried ajax, I think, but from what I can tell I'm doing it horribly wrong, or I'm not doing it at all but thinking that I am. I really don't know anymore. I'm so drained.

the main issues I face are with Message is Required / Body is Required error showing up even when the subject and body are filled upon submission, with an occasional An error occurred: JSON.parse: unexpected character at line 1 column 1 of the JSON data that I've found fixes for (.send vs .json) but overall I really just want to scrap the whole script and start anew in a better direction than what I've been trying

* I also tried curl -X POST https://SITENAMEREDACTED.com/send-email -F "subject=Test Subject" -F "body=Test message" to see if that would force it to notice the text inside the subject and body but that returns the same error:

{"errors":[{"type":"field","msg":"Subject is required","path":"subject","location":"body"},{"type":"field","msg":"Message body is required","path":"body","location":"body"}]

you can find the site here if you want to see: SITENAMEREDACTED

the 3rd box for the form is the honeypot (visible for testing) so if you submit a form don't enter anything into that box

i upped the max on the rate limit to 10 submits per 24 hours so you can test it a few times, but right now the script being used has the json error fixed, just has the main error of "Subject is required, Message body is required" showing up despite the subject and body being filled in upon submission

SOLUTION FOUND: NEW WORKING CODE:

<form id="contactForm" action="/send-email" method="POST" style="z-index: 200;">
  <div>
    <input autocomplete="off" type="text" placeholder="Subject" name="subject" required />
  </div>
  <div>
    <textarea autocomplete="off" placeholder="Your message" name="body" required></textarea>
  </div>

                            <div class="oh-no-hun" >
                              <input tabindex="-1" autocomplete="off" type="text" name="body2" />
                            </div>
  <div>
    <button type="submit">Send a message</button>
  </div>
</form>

<div id="message" class="message"></div>


<script>
  document.getElementById('contactForm').addEventListener('submit', async function(event) {
    event.preventDefault(); // Prevent the default form submission

    const form = event.target;
    const formData = new FormData(form);
    const messageDiv = document.getElementById('message');

    // Convert FormData to URL-encoded string
    const urlEncodedData = new URLSearchParams(formData).toString();

    try {
      const response = await fetch(form.action, {
        method: form.method,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded', // Specify URL-encoded content type
        },
        body: urlEncodedData, // Use URL-encoded string as the body
      });

      // Ensure response is in JSON format and read it
      const contentType = response.headers.get('content-type');
      let responseJson = {};

      if (contentType && contentType.includes('application/json')) {
        try {
          responseJson = await response.json();
        } catch (jsonError) {
          console.error('Failed to parse JSON:', jsonError);
          messageDiv.innerHTML = `<p class="error">Failed to parse server response.</p>`;
          return;
        }
      } else {
        const responseText = await response.text(); // Read as text if not JSON
        console.error('Unexpected response format:', responseText);
        messageDiv.innerHTML = `<p class="error">Unexpected response format. Response: ${responseText}</p>`;
        return;
      }

      if (response.ok) {
        // Display success message
        messageDiv.innerHTML = `<p class="success">${responseJson.message || 'Email sent successfully!'}</p>`;
        // Reset the form after successful submission
        form.reset();
      } else {
        // Handle and display validation errors
        const errors = responseJson.errors || [{ msg: 'An unknown error occurred.' }];
        messageDiv.innerHTML = `<p class="error">${errors.map(err => err.msg).join(', ')}</p>`;
      }

    } catch (error) {
      messageDiv.innerHTML = `<p class="error">An unexpected error occurred: ${error.message}</p>`;
    }
  });
</script>



server.js:

    const express = require('express');
const nodemailer = require('nodemailer');
const path = require('path');
const { body, validationResult } = require('express-validator'); // For validation
require('dotenv').config(); // Load environment variables from .env file
const rateLimit = require('express-rate-limit'); // For rate limiting

const app = express();
const port = process.env.PORT; // Use environment variable

// Reusable validation runner
const validate = validations => {
  return async (req, res, next) => {
    for (const validation of validations) {
      const result = await validation.run(req);
      if (!result.isEmpty()) {
        return res.status(400).json({ errors: result.array() });
      }
    }
    next();
  };
};

// See end-user IP rather than Cloudflare IP when limiting form submissions by IP
app.set('trust proxy', true);

// Serve static files (like your HTML form)
app.use(express.static(path.join(__dirname, 'public')));

// Middleware to parse form data
app.use(express.urlencoded({ extended: true })); // For x-www-form-urlencoded
app.use(express.json()); // For application/json

// Configure rate limiter
const formSubmitLimiter = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 24 hours
  max: 10, // Limit each IP to 10 requests per `windowMs`
  standardHeaders: true, // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
  message: 'Too many form submissions from this device, please try again later.',
});

// Route to handle form submission
app.post('/send-email',
  // Honeypot check
  (req, res, next) => {
    // Check if req.body.body2 has any content
    if (req.body.body2 && req.body.body2.length > 0) {
      return res.status(400).json({ message: 'Form submission failed.' });
  }
    // If body2 is empty, pass control to the next middleware
    next();
},
  
  // Apply rate limiter to the route
  formSubmitLimiter,

  // Validation middleware
  validate([
    body('subject').isLength({ min: 1 }).withMessage('Subject is required'),
    body('body').isLength({ min: 1 }).withMessage('Message body is required')
  ]),

  async (req, res) => {
    const errors = validationResult(req); // Check validation errors
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    const { subject, body } = req.body;

    // Configure nodemailer transport for SMTP service
    let transporter = nodemailer.createTransport({
      service: process.env.SMTP_SERVICE,
      auth: {
        user: process.env.EMAIL_USER,  // Use environment variable
        pass: process.env.EMAIL_PASS   // Use environment variable
      }
    });

    try {
      // Send email
      let info = await transporter.sendMail({
        from: `"SITENAMEREDACTED" <${process.env.EMAIL_USER}>`,  // Use environment variable
        to: process.env.RECIPIENT_EMAIL,                // Use environment variable
        subject: subject,
        text: body,
      });

      console.log('Message sent: %s', info.messageId);
      res.status(200).json({ message: 'Email sent successfully!' });
    } catch (error) {
      console.error('Error sending email:', error);
      res.status(500).json({ message: 'Error sending email' });
    }
  }
);

// Start server
app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`);
});

Issue: communication error between script and server (JSON vs urlencoded) - fixed with translation


Solution

  • Everything looks fine except for one thing: You are sending JSON. But you configured your server to parse only x-www-form-urlencoded format. Either send urlencoded on the client or enable JSON parsing on the server.

    (Your curl example didn't work either because -F is used to send multipart/formdata and not application/x-www-form-urlencoded and you haven't enable multipart parsing on your server either.)

    Perhaps you should validate the content type header on your server and return a proper error if it's not an expected format, then you'd catch this type of problem quicker. Comparing a working request with a broken request in the devtools or a debug proxy like Fiddler would also help.