Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Honeypot Laravel Package

spatie/laravel-honeypot

Spatie's laravel-honeypot blocks spam form submissions using an invisible honeypot field plus a timestamp check. Just add the x-honeypot Blade component to your form.

View on GitHub
Deep Wiki
Context7

Getting Started

spatie/laravel-honeypot has a small day-to-day surface: one Blade component/directive, one middleware, and one config file. The normal integration is three steps.

composer require spatie/laravel-honeypot

# optional, but recommended once you need to tune names/timing
php artisan vendor:publish --provider="Spatie\Honeypot\HoneypotServiceProvider" --tag="honeypot-config"

Add the honeypot fields to the form:

<form method="POST" action="{{ route('contact.store') }}">
    @csrf
    <x-honeypot />

    <input name="email" type="email">
    <textarea name="message"></textarea>

    <button type="submit">Send</button>
</form>

Protect the submission route:

use Spatie\Honeypot\ProtectAgainstSpam;

Route::post('/contact', ContactController::class)
    ->middleware(ProtectAgainstSpam::class);

On Laravel 11+, registering an alias keeps routes readable:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'honeypot' => \Spatie\Honeypot\ProtectAgainstSpam::class,
    ]);
})
Route::post('/contact', ContactController::class)->middleware('honeypot');

The first thing to verify is the failure path: submit the form with the hidden field filled in via DevTools or curl, and confirm the request never reaches your controller. By default you get a blank 200 response. Then submit normally and confirm the controller runs.

Where to look first:

  • config/honeypot.php for field names, timing, enabled state, responder class, and protection class.
  • Spatie\Honeypot\Honeypot for the generated field names and encrypted timestamp passed to the view.
  • Spatie\Honeypot\ProtectAgainstSpam and Spatie\Honeypot\SpamProtection for the actual detection rules.
  • The README sections for Inertia, Jetstream, Livewire, and CSP if your stack is not plain Blade.

For local development, set HONEYPOT_ENABLED=false if you regularly test forms with Postman/curl and do not want to manually include the fields. Keep it enabled in production. A default amount_of_seconds of 1 is aggressive; 2 or 3 is usually a better production default.

Implementation Patterns

Route-level protection for ordinary Blade forms. This is the default and safest pattern. Apply ProtectAgainstSpam only to routes that submit a protected form. If you have several public forms, group them:

Route::middleware(ProtectAgainstSpam::class)->group(function () {
    Route::post('/contact', [ContactController::class, 'store']);
    Route::post('/newsletter', [NewsletterController::class, 'store']);
    Route::post('/support', [SupportController::class, 'store']);
});

Global middleware when most POSTs are forms. If the app has many public forms and you control all of them, append the middleware to the web group or globally. This reduces the chance of forgetting it on a new route. The trade-off is that every POST in scope—logout, password reset, webhooks, API callbacks—now goes through the honeypot check. That is fine while honeypot_fields_required_for_all_forms is false, because missing fields are not automatically spam. It becomes dangerous when that flag is true.

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \Spatie\Honeypot\ProtectAgainstSpam::class,
    ]);
})

Inertia. Inject or share the Honeypot object and include its fields in the form payload. The object exposes enabled, nameFieldName, validFromFieldName, and encryptedValidFrom.

use Spatie\Honeypot\Honeypot;

public function create(Honeypot $honeypot)
{
    return inertia('Contact/Create', [
        'honeypot' => $honeypot,
    ]);
}

For a shared Inertia prop:

public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'honeypot' => new Honeypot(config('honeypot')),
    ]);
}

In Vue/React, initialize the form with dynamic keys and submit them on every request:

const form = useForm({
    email: '',
    message: '',
    [props.honeypot.nameFieldName]: '',
    [props.honeypot.validFromFieldName]: props.honeypot.encryptedValidFrom,
});

Render the hidden inputs from the same props. The important part is that the values are captured when the page is rendered and included in the POST—do not regenerate or overwrite them on every partial reload.

Livewire. Use the provided trait and a public HoneypotData property. Call protectAgainstSpam() before validation or persistence.

use Spatie\Honeypot\Http\Livewire\Concerns\HoneypotData;
use Spatie\Honeypot\Http\Livewire\Concerns\UsesSpamProtection;

class ContactForm extends Component
{
    use UsesSpamProtection;

    public HoneypotData $extraFields;

    public function mount(): void
    {
        $this->extraFields = new HoneypotData();
    }

    public function submit(): void
    {
        $this->protectAgainstSpam();

        $this->validate();

        // persist
    }
}
<form wire:submit="submit">
    <x-honeypot livewire-model="extraFields" />
    ...
</form>

For Volt functional syntax, return the HoneypotData property from guessHoneypotDataProperty, as shown in the README. Without that, the trait cannot find the bound data.

Custom spam responder. The default BlankPageResponder returns an empty 200. Replace it when you need logging, a redirect with a generic error, or a JSON response for APIs.

namespace App\Support;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Spatie\Honeypot\SpamResponder\SpamResponder;

class LogSpamResponder implements SpamResponder
{
    public function respond(Request $request, Closure $next): Response
    {
        logger()->warning('Honeypot triggered', [
            'ip' => $request->ip(),
            'path' => $request->path(),
            'user_agent' => $request->userAgent(),
        ]);

        return response()->json(['message' => 'Request rejected.'], 422);
    }
}
// config/honeypot.php
'respond_to_spam_with' => \App\Support\LogSpamResponder::class,

Keep the client-facing message generic. Do not tell the caller which honeypot rule fired.

Custom protection logic. The spam_protection config points at a class implementing the detection rules. Subclass the default and add your own checks—blocked IPs, suspicious user agents, disposable email domains—while preserving the built-in honeypot and timing checks.

namespace App\Support;

use Illuminate\Http\Request;
use Spatie\Honeypot\Exceptions\SpamException;
use Spatie\Honeypot\SpamProtection as BaseSpamProtection;

class SpamProtection extends BaseSpamProtection
{
    public function detect(Request $request): void
    {
        parent::detect($request);

        if (str_contains((string) $request->userAgent(), 'python-requests')) {
            throw new SpamException();
        }
    }
}
'spam_protection' => \App\Support\SpamProtection::class,

Testing. For unrelated feature tests, disable the honeypot in the test environment so you are not forced to post generated fields everywhere. For dedicated anti-spam tests, build a real Honeypot instance and post its values:

$honeypot = app(\Spatie\Honeypot\Honeypot::class);

$this->travel(2)->seconds();

$this->post('/contact', [
    $honeypot->nameFieldName => '',
    $honeypot->validFromFieldName => $honeypot->encryptedValidFrom,
    'email' => '[email protected]',
    'message' => 'Hello',
])->assertRedirect('/thanks');

Test the spam path separately by filling the name field or submitting without waiting:

$this->post('/contact', [
    'my_name' => 'bot',
])->assertOk(); // default BlankPageResponder returns an empty 200

Complementary protections. Use the honeypot with Laravel rate limiting, CSRF, validation, and—for high-risk public forms—a captcha or manual moderation queue. The honeypot is a cheap first filter, not a complete anti-spam system.

Gotchas and Tips

The default failure response is intentionally silent. BlankPageResponder returns an empty 200, not a 403 or validation error. That is good for bots, but it makes debugging confusing: the request “succeeds” from the browser’s perspective but the controller never runs. During development, use a custom responder that logs, or temporarily set HONEYPOT_ENABLED=false to isolate the issue.

Randomized field names are matched by prefix. With randomize_name_field_name enabled, the rendered field is something like my_name_<random>, and the middleware finds the submitted field by checking request keys that start with the configured base name. Avoid a base name that is a prefix of a real field. For example, a base of my_name can collide with a legitimate my_name_first field. Use a deliberately obscure base such as hp_website_url and do not reuse it for real inputs.

The config key names are easy to misread. valid_from_timestamp is a boolean toggle for the timing check. The actual submitted field name is valid_from_field_name, defaulting to valid_from. The README prose is slightly misleading when it says the component adds a valid_from_timestamp field. Inspect the rendered HTML if you are unsure which field names your form is submitting.

honeypot_fields_required_for_all_forms is the highest-risk setting. When enabled, any request through the middleware that is missing the honeypot fields is treated as spam. Combined with global middleware, this can break logout, password reset, API endpoints, webhooks, health checks, and Livewire’s /livewire/update endpoint. Only enable it if every POST that passes through the middleware includes the fields, and exclude routes that do not. In Livewire apps, prefer component-level protection over a global required-fields policy.

The timing check uses integer seconds. The default amount_of_seconds of 1 can flag legitimate users who submit very quickly, especially with password managers, autofill, or browser pre-rendering. 2 or 3 is safer. Setting it to 0 effectively disables the timing check. Browser tests and Dusk tests also submit quickly, so set HONEYPOT_SECONDS=0 in the test environment or advance time explicitly.

Missing or empty valid_from values are usually skipped, not rejected. The default timing check only fires when a timestamp is present and decryptable. A sophisticated bot can omit the field and avoid the time trap. Do not treat the timestamp as an enforced security token; if you need presence enforcement, add it in a custom SpamProtection class.

Do not store or share a single Honeypot instance across requests. The object captures an encrypted timestamp at construction time. In a normal request lifecycle that is fine. Under Octane or long-lived workers, verify that the container binding is request-scoped; a stale singleton timestamp can make every form appear instantly submitted or produce old data. Constructing a fresh Honeypot per request in Inertia share() is the safer pattern.

Full-page caching and key rotation. A cached form contains an old encrypted timestamp. That is generally fine because the package only checks a minimum elapsed time, not a maximum age. However, if APP_KEY rotates while a cached form is still being served, decryption of the timestamp can fail. Purge full-page caches after key rotation, and do not cache forms across environments with different keys.

CSP can un-hide the honeypot. The component hides fields with an inline style="display:none". A strict Content Security Policy can block that inline style, making the fields visible to users. Enable with_csp only if you have spatie/laravel-csp installed and configured, or render the fields manually with an allowed class/nonce. Test the rendered form under your actual CSP.

Inertia partial reloads can regenerate the object. If honeypot is shared globally, every Inertia response creates a new Honeypot object. Initialize the form from the initial props and do not overwrite the already-submitted hidden values on every prop update. The hidden fields must be part of the form state that is actually posted.

Livewire needs the data property to be discoverable. In class components, initialize the public HoneypotData property in mount() and call protectAgainstSpam() before validation and persistence. In Volt functional syntax, return the property from guessHoneypotDataProperty. Also note that the Livewire path aborts the request rather than necessarily using your configured HTTP SpamResponder; test the behavior your users see.

Autofill and accessibility. Some password managers and browsers can fill hidden fields. Use tabindex="-1" and autocomplete="off" on the honeypot inputs, and do not associate labels with them. If the package’s default markup does not include those attributes, render the fields manually using the Honeypot object’s public properties.

APIs need a JSON responder. The default blank 200 is a poor API response. Use a custom SpamResponder that returns a generic 422 or 403 JSON body, and apply the middleware only to the API routes that expect form-style submissions. Do not put a required-fields honeypot policy in front of signed webhooks.

Logging spam can capture PII. A custom responder is the right place to log IP, path, and user agent, but avoid dumping the entire request payload. Public forms may contain email addresses, phone numbers, or message bodies.

The package is a filter, not a wall. Dumb bots fill every field and get caught. Slightly smarter bots skip hidden fields and submit after a delay, and they will pass. Combine the honeypot with rate limiting, CSRF, validation, and moderation. For high-value forms, add a captcha or a challenge that is harder to bypass.

Debugging checklist. If legitimate submissions are rejected: confirm the middleware is on the route, HONEYPOT_ENABLED is correct, the form actually renders both fields, the request payload contains the generated field names, amount_of_seconds is not too high, and config caching is not serving stale values. If spam is getting through: inspect whether the bot is filling the name field, whether it waits longer than the threshold, and whether your global middleware scope actually includes the route.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
gam/laravel-sat-catalogs
cloudcreativity/laravel-stripe
abdulelahragih/querybuilder
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
shopwwi/laravel-cache
instance-code/repository
flooris/prestashop-models
lab2view/laravel-generator
lenderspender/laravel-wizard
joecampo/laravel-notification-basecamp
square1/laravel-collection-rolling-average