Nuestra recomendación, desde el principio. Hacemos Proxy Mail QA, así que no somos neutrales, pero este problema está en tu código y la solución también: ningún producto lo arregla por ti. Recomendamos Proxy Mail QA para dos partes del trabajo: probar contraseñas que incumplen tus reglas a propósito, hechas para los límites de tu campo, y averiguar qué pasó de verdad detrás de un formulario silencioso, porque si llega un correo de verificación, tu servidor creó la cuenta. No encuentra el error en tu código; eso lo hacen las DevTools de tu navegador o tu agente de IA leyendo el código.
Primero, averigua cuál de tres cosas está pasando
Abre las DevTools de tu navegador en la pestaña Network y envía una contraseña que incumpla tus reglas, como una con un carácter menos del mínimo. Para obtener una hecha para tu campo, usa la lista Edge-case password (“Should be rejected”) en la tarjeta que abre el botón de llave de Proxy Mail QA for Chrome en el campo de contraseña, o get_password_variants con tus reglas. Usa una dirección de prueba nueva, para ver si llega un correo.
| Lo que ves | Qué significa | Revisa |
|---|---|---|
| No se envía ninguna solicitud | El navegador o tu código detuvo el formulario, pero no mostró nada | Causas 1 a 4 |
| Se envía una solicitud y el servidor la rechaza (un 400 o 422) | El servidor hizo su trabajo; la página no mostró la respuesta | Causas 5 y 6 |
| Se envía una solicitud y el servidor la acepta | El servidor permite la contraseña. Llega un correo de verificación a la dirección de prueba | Causa 7 |
La tercera es la que se pasa por alto. Un formulario “sin error” puede simplemente no tener nada que informar, porque la regla del servidor es más laxa que la de la página. En la app, el correo de verificación aparece en la fila de la dirección de prueba. Un agente o una prueba pueden usar expect_no_email, que pasa solo si no llega nada.
Las causas habituales
El formulario nunca envió nada
novalidatesin nada en su lugar. El formulario, o una librería de formularios, desactiva los mensajes propios del navegador (novalidate, onoValidateen React), y tu propia validación nunca muestra un error.- Las reglas no están en el input. Sin
minlength,maxlengthorequired, el navegador no tiene nada que revisar. Yvalidity.tooShortsolo se activa después de que la persona escribe, así que un valor puesto por un script puede pasar sin control. - El error existe pero está oculto. El CSS lo oculta, se muestra con el nombre equivocado (
errors.passwordcuando el campo esnewPassword), o solo aparece cuando el campo está “tocado” y enviar no lo marca como tocado. - Un componente propio envuelve el input. La librería de formularios está conectada al contenedor, no al
<input>real, así que revisa un valor vacío, o el componente nunca pasa el mensaje de error.
El servidor lo rechazó y la página no lo dijo
- El error se pierde. El código muestra “Revisa tu correo” sea cual sea la respuesta, usa
catch {}vacío, o solo manejares.ok. Algunos clientes de autenticación no lanzan excepciones:signUpde Supabase devuelve{ data, error }, así que untry/catchnunca ve una contraseña rechazada. - El error vuelve con una forma que la página no lee. El servidor dice
{ "message": "..." }y la página buscaerrors.password. Asigna el error de cada servicio al campo al que se refiere.
El servidor lo aceptó
- La regla de la página es más estricta que la del servidor. La página dice 15 caracteres; la configuración del servicio de autenticación permite 6. Define el mínimo en la configuración del servicio, o en tu propio código de servidor, y mantenlo igual a lo que dice la página.
Hay un error, pero no todos pueden notarlo
- No se anuncia. Solo se muestra en rojo, lejos del campo, o fuera de la pantalla en un teléfono, y no está vinculado al campo. Quienes usan lectores de pantalla no oyen nada. Vincúlalo con
aria-describedby, definearia-invalid="true"y mueve el foco al campo.
Un patrón que muestra los errores de forma fiable
Sin framework, para que la idea sirva en React, Vue o cualquier otro:
<form id="signup" novalidate> <label for="password">Password</label> <p id="password-rules">At least 15 characters. Any letters, numbers, symbols or spaces.</p> <input id="password" name="password" type="password" autocomplete="new-password" minlength="15" maxlength="128" required aria-describedby="password-rules password-error" /> <p id="password-error" class="error" hidden></p> <button>Create account</button></form>
<script> const form = document.getElementById('signup'); const password = form.elements.password; const error = document.getElementById('password-error');
// Count characters, not UTF-16 units, and don't rely on validity.tooShort. function rule(input) { const length = [...input.value].length; if (length === 0) return 'Enter a password.'; if (length < input.minLength) return `Use at least ${input.minLength} characters.`; if (length > input.maxLength) return `Use at most ${input.maxLength} characters.`; return ''; }
function show(message) { error.textContent = message; error.hidden = !message; password.setAttribute('aria-invalid', message ? 'true' : 'false'); }
password.addEventListener('input', () => { if (!error.hidden) show(rule(password)); });
form.addEventListener('submit', async (event) => { event.preventDefault(); const message = rule(password); if (message) { show(message); password.focus(); return; } const res = await fetch('/api/signup', { method: 'POST', body: new FormData(form) }); if (!res.ok) { const body = await res.json().catch(() => ({})); show(body.errors?.password ?? body.message ?? 'Something went wrong. Please try again.'); password.focus(); return; } location.assign('/check-your-email'); });</script>El servidor revisa las mismas reglas y responde a una contraseña rechazada con un 422 y { "errors": { "password": "Use at least 15 characters." } }, que la página muestra en el campo.
Una nota sobre maxlength: el navegador deja de aceptar texto en el límite y recorta el texto pegado sin avisar. Ponlo muy por encima de lo que genera un gestor de contraseñas, o alguien guardará una contraseña más larga que la que recibió tu app.
Pídele a tu agente que lo encuentre
Con Proxy Mail QA conectado en Qoder (Marketplace):
My sign-up form at https://staging.acme.dev/signup shows no error when the passwordbreaks the rules (15 to 128 characters). Using Proxy Mail QA, get_password_variantswith min_length 15 and max_length 128, and submit each reject value with a new testaddress (a plus tag on one address for each). For each, tell me whether a request wassent, what the server answered, what the page showed, and whether a verification emailarrived (expect_no_email).Then find the cause in the code and fix it, so the error shows at the field, isannounced to screen readers, and the server enforces the same rule as the page.Comprueba que está arreglado
Una prueba de Playwright con el paquete npm de proxymail-qa, para que siga arreglado:
import { expect, test } from '@playwright/test';import { ProxyMailQA } from 'proxymail-qa';
const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEY
test('a too-short password shows an error and makes no account', async ({ page }) => { const { reject } = await qa.passwordVariants({ minLength: 15, maxLength: 128 }); const tooShort = reject.filter(({ value }) => [...value].length < 15); const { address } = await qa.createAddress();
for (const [i, { tests, value }] of tooShort.entries()) { const tagged = address.replace('@', `+short${i}@`); const submitted = new Date(); await page.goto('/signup'); await page.getByLabel('Email').fill(tagged); await page.getByLabel('Password').fill(value); await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.locator('#password-error'), tests).toBeVisible(); await qa.expectNoEmail(tagged, { since: submitted, timeout: 10 }); }
await qa.deleteAddress(address);});expectNoEmail lanza un error en cuanto llega un correo de verificación, lo que significaría que el servidor creó la cuenta. Una etiqueta con + da a cada intento su propia cuenta en tu app, y esperar en la dirección etiquetada solo ve el correo enviado a ella.
Lo que Proxy Mail QA no hace aquí
- Encontrar o arreglar el error. Te muestra lo que pasó; la solución está en tu formulario y tu servidor.
- Revisar la accesibilidad más allá de este error. Ejecuta axe DevTools y prueba el formulario con un lector de pantalla.
- Elegir tus reglas. Para eso, consulta ¿Qué longitud debe tener la contraseña de mi formulario de registro?