{"record":{"id":"2392b12130961689","repo":"harry0703/MoneyPrinterTurbo","slug":"background-music-upload-must-be-binary","errorCode":null,"errorMessage":"background music upload must be binary","messagePattern":"background music upload must be binary","errorType":"validation","errorClass":"BgmUploadError","httpStatus":400,"severity":"error","filePath":"app/services/bgm.py","lineNumber":204,"sourceCode":"        try:\n            source.seek(0)\n        except (AttributeError, OSError) as exc:\n            raise BgmUploadError(\"background music upload is not seekable\") from exc\n\n        # 保留原始扩展名便于 FFmpeg 针对无容器头的 AAC 等格式选择正确的\n        # demuxer；临时文件仍放在目标目录，以保证最终 os.replace 是原子操作。\n        descriptor, temp_path = tempfile.mkstemp(\n            prefix=_INTERNAL_UPLOAD_PREFIX,\n            suffix=Path(safe_name).suffix.lower(),\n            dir=target_dir,\n        )\n        with os.fdopen(descriptor, \"wb\") as output:\n            while True:\n                chunk = source.read(_COPY_CHUNK_BYTES)\n                if not chunk:\n                    break\n                if not isinstance(chunk, (bytes, bytearray, memoryview)):\n                    raise BgmUploadError(\"background music upload must be binary\")\n                total_bytes += len(chunk)\n                if total_bytes > MAX_BGM_UPLOAD_BYTES:\n                    raise BgmUploadError(\"background music file exceeds the 30 MB limit\")\n                output.write(chunk)\n            output.flush()\n            os.fsync(output.fileno())\n\n        if total_bytes == 0:\n            raise BgmUploadError(\"background music file is empty\")\n        return safe_name, temp_path, total_bytes\n    except Exception as exc:\n        _remove_staged_file(temp_path)\n        if isinstance(exc, BgmUploadError):\n            raise\n        if isinstance(exc, OSError):\n            raise BgmServiceError(\"failed to stage background music upload\") from exc\n        raise\n    finally:","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/bgm.py#L186-L222","documentation":"Raised by stage_bgm_upload while streaming an upload to a temp file: a chunk returned by source.read() was not bytes, bytearray, or memoryview. This guards against callers passing a text-mode file object (io.StringIO, open(..., 'r')) or a mock/BytesIO-like object whose read() returns str. It fails mid-copy, so the staged temp file is removed and the original UploadedFile pointer is later restored in the finally block.","triggerScenarios":"Calling the BGM upload staging function with a file opened in text mode ('rb' omitted), an io.StringIO, or a custom BinaryIO stub whose read() returns str. Any non-bytes chunk on the very first or a later 1 MB read triggers it.","commonSituations":"FastAPI/Streamlit handlers re-wrapping the upload in a TextIOWrapper; test fixtures mocking the upload source with str chunks; passing an already-decoded payload (base64 str) instead of raw bytes.","solutions":["Open/pass the upload source in binary mode: open(path, 'rb') or the original spooling file from the request, never a text wrapper.","If the payload arrives as base64 or text, decode to bytes (base64.b64decode(...)) and wrap with io.BytesIO before calling the staging API.","In tests, make the fake source return b'...' chunks (e.g. io.BytesIO(b'audio')) so read() yields bytes."],"exampleFix":"// before\nsource = open(upload_path, 'r')  # text mode -> str chunks\nsafe_name, temp_path, size = stage_bgm_upload(source, upload.filename)\n\n// after\nsource = open(upload_path, 'rb')  # binary mode -> bytes chunks\nsafe_name, temp_path, size = stage_bgm_upload(source, upload.filename)","handlingStrategy":"type-guard","validationCode":"def is_binary_source(source) -> bool:\n    probe = source.read(0)\n    source.seek(0) if hasattr(source, 'seek') else None\n    return isinstance(probe, (bytes, bytearray, memoryview))","typeGuard":"from typing import BinaryIO\n\ndef assert_binary_upload(source) -> None:\n    if not isinstance(source, (io.RawIOBase, io.BufferedIOBase)) and not hasattr(source, 'read'):\n        raise TypeError('upload source must be a binary file object')\n    sample = source.read(1)\n    if isinstance(sample, str):\n        raise TypeError('upload source is text-mode; reopen in \\'rb\\'')\n    if hasattr(source, 'seek'):\n        source.seek(0)","tryCatchPattern":"try:\n    stage_bgm_upload(source, filename)\nexcept BgmUploadError as e:\n    if 'must be binary' in str(e):\n        source = io.BytesIO(original_bytes)  # reopen as binary and retry once","preventionTips":["Always open uploads with mode='rb' and pass request bodies straight through without TextIO wrappers.","Decode base64/text payloads to bytes and wrap in io.BytesIO before staging.","Make test doubles return bytes chunks so type assumptions match production."],"tags":["bgm","upload","type-error","binary-io"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}