Brian Mwangi

Back to articles

Using honeypot fields in form inputs

-- 2 min read

An image of a spider web

If you have some kind of public form in your website e.g a contact form you have encountered numerous spam emails from bots (and people). There are malicious bots which will scrape the internet for public forms and try to submit them. This ends up flooding your email with garbage, diminishing your email deliverability, and wasting your server's precious bandwidth.

What is a honeypot field?

A honeypot field is a hidden input that typically a user would not see but a bot submitting a form would see. When using a honeypot field you would check to see if the honeypot input was filled and discard the email.

A very basic honeypot field would look like this:

<form method="post">
  <label>
    Message:
    <textarea name="message"/>
  </label>
      
  <!-- Honeypot field  -->
  <label style="display: none;">
    Do not fill out this field
    <input type="text" name="name__confirm" />
  </label>
      
  <button type="submit">Submit</button>
</form>

If by any chance a user saw the field there is a label telling them not to fill the field.😅

Practical implementation

In a production application one would not only check if the honeypot field was filled but also consider things like how fast the field was filled. This can get real ugly real quick. Let's use Remix to see how this can be implemented in a real application. If you're new to Remix, check out the get started guide here.

We will use remix-utils to help us render the honeypot fields in our application. Remix utils can keep up with the cat and mouse game of using honeypot fields and can handle the edge cases that may come up.

Let's start by installing remix-utils

npm install remix-utils

Now let's instantiate the Honeypot instance in the honeypot.js file.

// app/.server/honeypot.js

import { Honeypot } from "remix-utils/honeypot/server";

// Create a new Honeypot instance, the values here are the defaults, you can
// customize them
export const honeypot = new Honeypot({
	randomizeNameFieldName: false,
	nameFieldName: "name__confirm",
	validFromFieldName: "from__confirm", // null to disable it
	encryptionSeed: undefined, // Ideally it should be unique even between processes
});

The next step is to return the form input properties that will be used with the honeypot fields. We do this from the root loader.

// app/root.jsx

import { json } from "@remix-run/node";
import { honeypot } from "~/honeypot.server";

export async function loader() {
  return json({ honeypotInputProps: honeypot.getInputProps() });
}

In our root component we will need to render the HoneypotProvider which will wrap our entire application. This ensures that no matter where you're rendering a form you can have access to the honeypot fields.

// app/root.jsx

import { useLoaderData } from "@remix-run/react";
import { HoneypotProvider } from "remix-utils/honeypot/react";

export default function App() {
  let { honeypotInputProps } = useLoaderData();
  return (
    <html>
      <head>
    </head>
    <body>
      <HoneypotProvider {...honepotInputProps}>
        <Outlet />
      <HoneypotProvider>
    </html>
  );
}

The setup is complete. Now we're ready to use honeypot fields anywhere in our application. Remix-utils has a <HoneypotInputs /> component for that. Let's use it in a contact form.

// app/contact.jsx

import { Form } from "@remix-run/react";
import { HoneypotInputs } from "remix-utils/honeypot/react";

export default function Contact() {
  return (
    <main>
      <Form method="post">
        <HoneypotInputs />
    
        <label>
          Name:
          <input type="text" name="name" />
        </label>
        <label>
            Email:
            <input type="email" name="email" />
        </label>
        <label>
            Message:
            <textarea name="message" />
        </label>
        <button type="submit">Submit</button>
      </Form>
    </main>
  );
}

We can check if the honeypot input was filled the form is submitted and discard the action.

We will check that in the action function in the contact route.

// app/contact.jsx

import { SpamError } from "remix-utils/honeypot/server";
import { honeypot } from "~/honeypot.server";

export async function action({ request }) {
  let formData = await request.formData();

  try {
    honeypot.check(formData);
  } catch (error) {
    if (error instanceof SpamError) {
      throw new Response('Form not submitted properly', { status: 400 });
    }
    throw error;
  }

  // Send email
  await sendEmail();
  
  return null;
}

That's all! 🙂. Now we have protected our forms against spamming bots.

WhatsApp icon