{"record":{"id":"75ad7700be3c8cb2","repo":"ZhuLinsen/daily_stock_analysis","slug":"validation-error","errorCode":"validation_error","errorMessage":"symbol must not be empty","messagePattern":"symbol must not be empty","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"api/v1/endpoints/portfolio.py","lineNumber":503,"sourceCode":"    response = TaskAccepted(\n        task_id=task.task_id,\n        trace_id=task.trace_id or task.task_id,\n        status=\"pending\",\n        message=f\"分析任务已加入队列: {task.stock_code}\",\n        analysis_phase=task.analysis_phase,\n    )\n    return response\n\n\ndef _resolve_position_analysis_context(\n    service: PortfolioService,\n    *,\n    symbol: str,\n    account_id: Optional[int],\n) -> dict:\n    target = service._normalize_symbol_for_position(symbol)\n    if not target:\n        raise ValueError(\"symbol must not be empty\")\n\n    snapshot = service.get_portfolio_snapshot(account_id=account_id, cost_method=\"fifo\")\n    matches = []\n    for account in snapshot.get(\"accounts\") or []:\n        for position in account.get(\"positions\") or []:\n            position_symbol = service._normalize_symbol_for_position(\n                str(position.get(\"symbol\") or \"\")\n            )\n            if position_symbol != target:\n                continue\n            try:\n                quantity = float(position.get(\"quantity\") or 0)\n            except (TypeError, ValueError):\n                quantity = 0.0\n            if quantity <= 0:\n                continue\n            matches.append((account, position, position_symbol))\n","sourceCodeStart":485,"sourceCodeEnd":521,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/portfolio.py#L485-L521","documentation":"ValueError('symbol must not be empty') raised inside _resolve_position_analysis_context and mapped to a 400 validation_error by POST /portfolio/positions/{symbol}/analysis (portfolio.py:457-458). The path symbol is passed through PortfolioService._normalize_symbol_for_position; when normalization yields an empty string — empty/whitespace path segment, or a symbol consisting solely of characters normalization strips — the ValueError fires before any position lookup.","triggerScenarios":"POST /portfolio/positions//analysis (empty path segment); a symbol of only spaces or only punctuation that normalization reduces to ''; URL-encoded whitespace (%20) as the symbol; client interpolating an unset variable into the path.","commonSituations":"Frontend submitting before the symbol state is populated; scripts iterating symbol lists containing blanks; copy/paste introducing invisible characters that normalize away.","solutions":["Validate and trim the symbol client-side before POSTing; require a non-empty result.","URL-encode the symbol: /portfolio/positions/{encodeURIComponent(symbol)}/analysis.","If a legitimate symbol normalizes to empty, check _normalize_symbol_for_position's rules for that market's format (e.g. HK/A股 prefixes) and use the canonical form."],"exampleFix":"# before\nrequests.post(f\"{base}/portfolio/positions/{symbol}/analysis\", json={})\n\n# after\nsymbol = (symbol or '').strip()\nif not symbol:\n    raise ValueError('symbol is required')\nrequests.post(f\"{base}/portfolio/positions/{quote(symbol)}/analysis\", json={})","handlingStrategy":"validation","validationCode":"const symbol = String(rawSymbol ?? '').trim();\nif (!/^([0-9]{5,6}|[A-Z.\\-]{1,12})$/i.test(symbol)) {\n  throw new Error('symbol must be a non-empty ticker');\n}\nawait fetch(`/portfolio/positions/${encodeURIComponent(symbol)}/analysis`, { method: 'POST', body: '{}' });","typeGuard":"const isNonEmptySymbol = (v: unknown): v is string =>\n  typeof v === 'string' && v.trim().length > 0;","tryCatchPattern":"try { await analyzePosition(symbol); }\ncatch (e) { if (isHttp400(e) && /symbol must not be empty/.test(e.message)) flagBadSymbolInput(); }","preventionTips":["Trim and validate the symbol before building the path; disable submit on empty.","Always encodeURIComponent path parameters.","Use the symbol exactly as returned by the portfolio snapshot/positions endpoints so normalization matches.","Expect a 400 ValueError mapping here, a 404 when no non-zero position exists, and a 400 ambiguous_position_account when the symbol spans multiple accounts — handle each distinctly."],"tags":["http-400","validation","portfolio-api","symbol"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}