knadh/listmonk · error

failed to marshal Altcha challenge

Error message

failed to marshal Altcha challenge: %w

What it means

GenerateChallenge wraps an error from json.Marshal when serializing the freshly created altcha.Challenge struct to JSON. This is nearly impossible with a well-formed challenge struct and indicates a structural problem — typically an unsupported field type (e.g. channel, func, or cyclic data) in the challenge object or an out-of-memory condition.

Solutions

  1. Check the version of the altcha library and whether its Challenge type changed recently (go.mod diff).
  2. Marshal the challenge manually into a plain struct of JSON-safe fields if customization added non-serializable members.
  3. Unwrap with errors.As to confirm it is a *json.UnsupportedTypeError and log the offending field type.

Example fix

// before: marshal library struct directly
challengeJSON, err := json.Marshal(challenge)
// after: map to explicit JSON-safe struct
out := struct {
  Algorithm string `json:"algorithm"`
  Challenge string `json:"challenge"`
  Salt      string `json:"salt"`
  Signature string `json:"signature"`
}{challenge.Algorithm, challenge.Challenge, challenge.Salt, challenge.Signature}
challengeJSON, err := json.Marshal(out)
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: ensure the challenge marshals in a test
var _ = func() error {
  ch, err := altcha.Challenge(altcha.ChallengeOptions{MaxNumber: 1000, SaltLength: 8, HMACKey: "test"})
  if err != nil {
    return err
  }
  _, err = json.Marshal(ch)
  return err
}

Try / catch

challenge, err := captcha.GenerateChallenge(ctx)
if err != nil {
  if strings.Contains(err.Error(), "failed to marshal Altcha challenge") {
    log.Error("challenge not serializable — check altcha library version", "err", err)
    http.Error(w, "captcha unavailable", http.StatusInternalServerError)
    return
  }
}

Prevention

When it happens

Trigger: Calling GenerateChallenge with ProviderAltcha after altcha.Challenge succeeds but json.Marshal(challenge) fails, e.g. after a library upgrade changes the challenge struct to contain a non-serializable field.

Common situations: Upgrading the altcha dependency introduces a non-marshalable field; custom Challenge wrappers add fields like func or chan; extreme memory pressure.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


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

Appendix: source

Thrown at internal/captcha/captcha.go:134

// For Altcha, this returns a JSON challenge.
func (c *Captcha) GenerateChallenge() (string, error) {
	switch c.provider {
	case ProviderAltcha:
		exp := time.Now().Add(5 * time.Minute)
		challenge, err := altcha.CreateChallenge(altcha.ChallengeOptions{
			Algorithm:  altcha.SHA256,
			MaxNumber:  int64(c.altcha.Complexity),
			SaltLength: 12,
			HMACKey:    c.altcha.HMACKey,
			Expires:    &exp,
		})
		if err != nil {
			return "", fmt.Errorf("failed to create Altcha challenge: %w", err)
		}

		challengeJSON, err := json.Marshal(challenge)
		if err != nil {
			return "", fmt.Errorf("failed to marshal Altcha challenge: %w", err)
		}

		return string(challengeJSON), nil
	case ProviderHCaptcha:
		// hCaptcha generates challenges client-side.
		return "", nil
	default:
		return "", fmt.Errorf("no captcha provider enabled")
	}
}

// Verify verifies a CAPTCHA response.
func (c *Captcha) Verify(token string) (error, bool) {
	switch c.provider {
	case ProviderAltcha:
		return c.verifyAltcha(token)
	case ProviderHCaptcha:
		return c.verifyHCaptcha(token)

View on GitHub (pinned to 670c01717d)