ZhuLinsen/daily_stock_analysis · warning · HTTPException

bad_request

bad_request

Error message

未提供文件,请使用表单字段 file 上传图片

What it means

Raised by POST /api/v1/stocks/extract-from-image when the multipart form contains no field named file, or the field has no filename. The endpoint declares file as Optional[UploadFile] specifically so it can return this friendly 400 (bad_request) instead of FastAPI's default 422. It is purely a request-shape error; no bytes are read.

Source

Thrown at api/v1/endpoints/stocks.py:142

    responses={
        200: {"description": "提取的股票代码"},
        400: {"description": "图片无效", "model": ErrorResponse},
        500: {"description": "服务器错误", "model": ErrorResponse},
    },
    summary="从图片提取股票代码",
    description="上传截图/图片,通过 Vision LLM 提取股票代码。支持 JPEG、PNG、WebP、GIF,最大 5MB。",
)
def extract_from_image(
    file: Optional[UploadFile] = File(None, description="图片文件(表单字段名 file)"),
    include_raw: bool = Query(False, description="是否在结果中包含原始 LLM 响应"),
) -> ExtractFromImageResponse:
    """
    从上传的图片中提取股票代码(使用 Vision LLM)。

    表单字段请使用 file 上传图片。优先级:Gemini / Anthropic / OpenAI(首个可用)。
    """
    if not file or not file.filename:
        raise HTTPException(
            status_code=400,
            detail={"error": "bad_request", "message": "未提供文件,请使用表单字段 file 上传图片"},
        )

    content_type = (file.content_type or "").split(";")[0].strip().lower()
    if content_type not in ALLOWED_MIME:
        raise HTTPException(
            status_code=400,
            detail={
                "error": "unsupported_type",
                "message": f"不支持的类型: {content_type}。允许: {ALLOWED_MIME_STR}",
            },
        )

    try:
        # 先读取限定大小,再检查是否还有剩余(语义清晰:超出则拒绝)
        data = file.file.read(MAX_SIZE_BYTES)
        if file.file.read(1):

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Send multipart/form-data with the field named exactly 'file': curl -F 'file=@screenshot.png' .../extract-from-image.
  2. In JS, use formData.append('file', fileBlob, 'screenshot.png') so a filename is present.
  3. Double-check the endpoint: this route wants multipart, not application/json (that is /parse-import).

Example fix

// before
const fd = new FormData();
fd.append('image', file);

// after
const fd = new FormData();
fd.append('file', file, file.name);
Defensive patterns

Strategy: validation

Validate before calling

if (!(file instanceof File) || !file.name) {
  showError('请选择要上传的图片文件');
  return;
}
const fd = new FormData();
fd.append('file', file, file.name);

Type guard

function isUploadableFile(v: unknown): v is File {
  return v instanceof File && typeof v.name === 'string' && v.name.length > 0;
}

Prevention

When it happens

Trigger: Calling the endpoint with an empty body; sending the image under a different form field name (e.g. image, upload); sending JSON instead of multipart/form-data; some HTTP clients that omit filename in the Content-Disposition header.

Common situations: Frontend field name mismatch between the FormData append key and the API contract; curl invocations using -d instead of -F; automated tests posting JSON to a multipart endpoint; proxies stripping multipart parts.

Related errors


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