knadh/listmonk · warning

hCaptcha failed: %s

Error message

hCaptcha failed: %s

What it means

verifyHCaptcha returns this when the hCaptcha siteverify API responds with success=false. The response's error-codes array is joined into the message, explaining why hCaptcha rejected the token (bad secret, expired/already-used token, missing-input, etc.). This is the normal rejection path for failed human verification, not an infrastructure error.

Source

Thrown at internal/captcha/captcha.go:180

		"response": {token},
	})
	if err != nil {
		return err, false
	}

	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err, false
	}

	var r hCaptchaResp
	if err := json.Unmarshal(body, &r); err != nil {
		return err, true
	}

	if !r.Success {
		return fmt.Errorf("hCaptcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
	}

	return nil, true
}

// verifyAltcha verifies an Altcha response.
func (c *Captcha) verifyAltcha(payload string) (error, bool) {
	valid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)
	if err != nil {
		return fmt.Errorf("failed to verify captcha solution: %w", err), false
	}

	if !valid {
		return fmt.Errorf("captcha verification failed"), false
	}

	// Disallow token reuse.
	if _, err := tmptokens.Check(payload); err == nil {

View on GitHub (pinned to 670c01717d)

Solutions

  1. Read the joined error-codes in the message: 'invalid-input-secret' means fix the secret key; 'timeout-or-duplicate' means the token expired or was reused.
  2. For timeout-or-duplicate, ensure the token is submitted once — reset the hCaptcha widget and fetch a fresh token after each attempt.
  3. Verify the secret key matches the sitekey's environment (hCaptcha dashboard).
  4. Return a user-facing 'captcha failed, please retry' message and never treat this as a 5xx server error.
  5. Confirm the form actually posts the h-captcha-response field as the token.

Example fix

// before: generic rejection message
return fmt.Errorf("hCaptcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
// after: map common codes to actionable messages
switch {
case contains(r.ErrorCodes, "invalid-input-secret"):
  return errors.New("hcaptcha secret key is invalid"), false
case contains(r.ErrorCodes, "timeout-or-duplicate"):
  return errors.New("captcha token expired or reused, retry"), false
}
Defensive patterns

Strategy: try-catch

Validate before calling

token := r.PostFormValue("h-captcha-response")
if token == "" {
  return errors.New("hCaptcha token missing from form submission")
}
// optionally check token shape before calling siteverify
if len(token) < 20 {
  return errors.New("hCaptcha token malformed")
}

Type guard

func hCaptchaTokenPresent(r *http.Request) bool {
  return r.PostFormValue("h-captcha-response") != ""
}

Try / catch

if err, ok := captcha.Verify(token); !ok {
  if err != nil && strings.HasPrefix(err.Error(), "hCaptcha failed:") {
    switch {
    case strings.Contains(err.Error(), "timeout-or-duplicate"):
      // user retry: reset widget, new token
    case strings.Contains(err.Error(), "invalid-input-secret"):
      log.Error("wrong hCaptcha secret key — fix config")
    default:
      // likely a bot: reject normally
    }
    http.Error(w, "captcha failed, please retry", http.StatusBadRequest)
    return
  }
}

Prevention

When it happens

Trigger: Calling Verify with an hCaptcha token when hCaptcha's siteverify endpoint replies {"success": false, "error-codes": [...]}, e.g. the user failed the challenge, the token was already redeemed, the response token expired (~2 minutes), or the secret key is wrong.

Common situations: User submits form twice so the token is reused; slow form submission exceeds token lifetime; wrong secret key between environments (staging vs production); bots failing the challenge — the most common production occurrence.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/6cc2c2d11e2805cb. Report an issue: GitHub.