{"record":{"id":"005933a8d20ea3f1","repo":"ZhuLinsen/daily_stock_analysis","slug":"file-too-large","errorCode":"file_too_large","errorMessage":"图片超过 {MAX_SIZE_BYTES // (1024 * 1024)}MB 限制","messagePattern":"图片超过 (.+?)MB 限制","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"api/v1/endpoints/stocks.py","lineNumber":161,"sourceCode":"            status_code=400,\n            detail={\"error\": \"bad_request\", \"message\": \"未提供文件，请使用表单字段 file 上传图片\"},\n        )\n\n    content_type = (file.content_type or \"\").split(\";\")[0].strip().lower()\n    if content_type not in ALLOWED_MIME:\n        raise HTTPException(\n            status_code=400,\n            detail={\n                \"error\": \"unsupported_type\",\n                \"message\": f\"不支持的类型: {content_type}。允许: {ALLOWED_MIME_STR}\",\n            },\n        )\n\n    try:\n        # 先读取限定大小，再检查是否还有剩余（语义清晰：超出则拒绝）\n        data = file.file.read(MAX_SIZE_BYTES)\n        if file.file.read(1):\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": \"file_too_large\",\n                    \"message\": f\"图片超过 {MAX_SIZE_BYTES // (1024 * 1024)}MB 限制\",\n                },\n            )\n    except HTTPException:\n        raise\n    except Exception as e:\n        logger.warning(f\"读取上传文件失败: {e}\")\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"read_failed\", \"message\": \"读取上传文件失败\"},\n        )\n\n    try:\n        items, raw_text = extract_stock_codes_from_image(data, content_type)\n        extract_items = [","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/stocks.py#L143-L179","documentation":"Raised by /extract-from-image when the uploaded image exceeds MAX_SIZE_BYTES (5MB per the route description). The handler reads exactly MAX_SIZE_BYTES, then probes one more byte; any extra byte proves the file is larger, so oversized files are rejected without loading them fully. HTTP 400 with error code file_too_large.","triggerScenarios":"POSTing an image >5MB; images between exactly 5MB and 5MB+1 byte (the boundary is inclusive: a file of exactly MAX_SIZE_BYTES passes); screenshots from retina displays saved as lossless PNG.","commonSituations":"Long trading screenshots concatenated into one tall PNG; scanned broker statements; users on mobile uploading original camera photos; GIFs with many frames.","solutions":["Compress or downscale the image client-side before upload (canvas resize or toBlob with quality ~0.8 JPEG).","Enforce the same 5MB limit in the UI and show the size before submit.","For PNG screenshots, convert to JPEG/WebP which typically shrinks well below 5MB."],"exampleFix":"// before\nfd.append('file', originalFile);\n\n// after\nasync function shrink(file, maxBytes = 5 * 1024 * 1024) {\n  if (file.size <= maxBytes) return file;\n  const img = await createImageBitmap(file);\n  const scale = Math.sqrt(maxBytes / file.size) * 0.95;\n  const canvas = new OffscreenCanvas(Math.round(img.width * scale), Math.round(img.height * scale));\n  canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);\n  return canvas.convertToBlob({type: 'image/jpeg', quality: 0.85});\n}\nfd.append('file', await shrink(originalFile));","handlingStrategy":"validation","validationCode":"const MAX = 5 * 1024 * 1024;\nif (file.size > MAX) {\n  showError(`图片超过 5MB 限制（当前 ${(file.size / 1048576).toFixed(1)}MB）`);\n  return;\n}","typeGuard":"function isWithinImageLimit(file: File, max = 5 * 1024 * 1024): boolean {\n  return file.size <= max;\n}","tryCatchPattern":null,"preventionTips":["Check file.size before upload; downscale/compress when over the limit.","Show the size in the UI after selection.","Prefer JPEG/WebP for screenshots to stay well under 5MB."],"tags":["upload","size-limit","image","http-400"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}