ZhuLinsen/daily_stock_analysis · error · ValueError

{field_name} must be at most {max_length} characters

Error message

{field_name} must be at most {max_length} characters

What it means

ValueError from DecisionSignalService._optional_text (src/services/decision_signal_service.py:1199): optional free-text fields (notes, reasons, etc.) that are present and non-blank must not exceed max_length characters after stripping. Whitespace-only values are treated as absent (return None); anything longer than the cap raises '{field_name} must be at most {max_length} characters'. Each call site dictates the cap.

Source

Thrown at src/services/decision_signal_service.py:1199

        if not text:
            raise ValueError("trigger_source is required")
        return text

    @classmethod
    def _normalize_optional_trigger_source(cls, value: Any) -> Optional[str]:
        if value in (None, ""):
            return None
        return cls._normalize_trigger_source(value)

    @staticmethod
    def _optional_text(value: Any, field_name: str, *, max_length: int) -> Optional[str]:
        if value is None:
            return None
        text = str(value).strip()
        if not text:
            return None
        if len(text) > max_length:
            raise ValueError(f"{field_name} must be at most {max_length} characters")
        return text

    @classmethod
    def _optional_public_text(cls, value: Any, field_name: str, *, max_length: int) -> Optional[str]:
        return cls._public_text(value, field_name, max_length=max_length, required=False)

    @staticmethod
    def _public_text(value: Any, field_name: str, *, max_length: int, required: bool) -> Optional[str]:
        if value is None:
            if required:
                raise ValueError(f"{field_name} is required")
            return None
        text = sanitize_decision_signal_text(value)
        if not text:
            if required:
                raise ValueError(f"{field_name} is required")
            return None
        if len(text) > max_length:

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Truncate client-side before the call: text.strip()[:max_length] (check the error message or field schema for the exact cap).
  2. Move long-form content into a field designed for it (e.g. structured metadata/JSON signal_text) instead of a capped text column.
  3. Bind frontend inputs to the same maxlength as the backend schema.
  4. For machine-generated text, summarize or hash-and-store instead of inlining.

Example fix

# before
service.create_signal({..., "notes": full_analysis_paragraph})  # 5000 chars > cap → ValueError

# after
notes = full_analysis_paragraph.strip()[:500] or None
service.create_signal({..., "notes": notes})
Defensive patterns

Strategy: validation

Validate before calling

MAX = 500  # use the cap from the field schema / error message
text = payload.get('notes')
if text is not None:
    text = str(text).strip() or None
    payload['notes'] = text[:MAX] if text else None

Type guard

def fits(text: str | None, max_length: int) -> bool:
    return text is None or 0 < len(text.strip()) <= max_length

Prevention

When it happens

Trigger: Sending a notes/reason string longer than the field's configured cap — e.g. pasting an entire LLM analysis paragraph into a short free-text field, or a full error traceback into a remarks field. Multi-byte content counts by character, not bytes, so CJK text hits the cap slower but still hits it.

Common situations: UI textarea without maxlength bound to a capped backend field; concatenating multiple reasons with '\n'.join() without trimming; logs forwarded into metadata text fields; changing the cap without notifying the frontend.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/955ceb2025f63cdd. Report an issue: GitHub.