{"record":{"id":"272504a1d72feb93","repo":"ZhuLinsen/daily_stock_analysis","slug":"read-failed","errorCode":"read_failed","errorMessage":"读取上传文件失败","messagePattern":"读取上传文件失败","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"api/v1/endpoints/stocks.py","lineNumber":172,"sourceCode":"            },\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 = [\n            ExtractItem(code=code, name=name, confidence=conf) for code, name, conf in items\n        ]\n        codes = [i.code for i in extract_items]\n        return ExtractFromImageResponse(\n            codes=codes,\n            items=extract_items,\n            raw_text=raw_text if include_raw else None,\n        )\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail={\"error\": \"extract_failed\", \"message\": str(e)})\n    except Exception as e:","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/api/v1/endpoints/stocks.py#L154-L190","documentation":"Raised by /extract-from-image when reading the request body's underlying file object raises an unexpected exception (the HTTPException for size is re-raised untouched; everything else lands here). It wraps transport/stream errors during file.file.read into a controlled 400 with error code read_failed and logs a warning '读取上传文件失败'.","triggerScenarios":"Client aborts the upload mid-body so the multipart stream is truncated; a reverse proxy (nginx client_body_temp) fails or times out while streaming; spooled temp file deleted or unwritable (starlette spools >1MB to disk); malformed multipart chunking.","commonSituations":"nginx proxy with client_max_body_size smaller than the upload and interrupted transfers; disk-full on the API container's temp dir; flaky mobile networks dropping connections; load balancer idle timeouts during slow uploads.","solutions":["Retry the request — transient stream errors often succeed on a second attempt.","Check proxy limits: set nginx client_max_body_size 6m and adequate proxy_read_timeout/proxy_send_timeout.","Verify the API container has writable temp space and free disk for spooled uploads."],"exampleFix":"# before (nginx)\nclient_max_body_size 1m;\n\n# after\nclient_max_body_size 6m;\nproxy_read_timeout 120s;","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp = None\nfor attempt in range(3):\n    try:\n        resp = requests.post(f'{base}/api/v1/stocks/extract-from-image',\n                             files={'file': fh}, timeout=60)\n        if resp.status_code != 400 or resp.json().get('detail', {}).get('error') != 'read_failed':\n            break\n    except requests.RequestException:\n        pass\n    time.sleep(2 ** attempt)\nassert resp is not None","preventionTips":["Keep nginx client_max_body_size above the app's 5MB image limit.","Ensure the API host has writable temp space for spooled uploads.","Treat read_failed as transient first; escalate only when retries keep failing."],"tags":["upload","network","proxy","http-400"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}