harry0703/MoneyPrinterTurbo · error · BgmUploadError

background music upload must be binary

Error message

background music upload must be binary

What it means

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.

Source

Thrown at app/services/bgm.py:204

        try:
            source.seek(0)
        except (AttributeError, OSError) as exc:
            raise BgmUploadError("background music upload is not seekable") from exc

        # 保留原始扩展名便于 FFmpeg 针对无容器头的 AAC 等格式选择正确的
        # demuxer;临时文件仍放在目标目录,以保证最终 os.replace 是原子操作。
        descriptor, temp_path = tempfile.mkstemp(
            prefix=_INTERNAL_UPLOAD_PREFIX,
            suffix=Path(safe_name).suffix.lower(),
            dir=target_dir,
        )
        with os.fdopen(descriptor, "wb") as output:
            while True:
                chunk = source.read(_COPY_CHUNK_BYTES)
                if not chunk:
                    break
                if not isinstance(chunk, (bytes, bytearray, memoryview)):
                    raise BgmUploadError("background music upload must be binary")
                total_bytes += len(chunk)
                if total_bytes > MAX_BGM_UPLOAD_BYTES:
                    raise BgmUploadError("background music file exceeds the 30 MB limit")
                output.write(chunk)
            output.flush()
            os.fsync(output.fileno())

        if total_bytes == 0:
            raise BgmUploadError("background music file is empty")
        return safe_name, temp_path, total_bytes
    except Exception as exc:
        _remove_staged_file(temp_path)
        if isinstance(exc, BgmUploadError):
            raise
        if isinstance(exc, OSError):
            raise BgmServiceError("failed to stage background music upload") from exc
        raise
    finally:

View on GitHub (pinned to 1f9f19c202)

Solutions

  1. Open/pass the upload source in binary mode: open(path, 'rb') or the original spooling file from the request, never a text wrapper.
  2. If the payload arrives as base64 or text, decode to bytes (base64.b64decode(...)) and wrap with io.BytesIO before calling the staging API.
  3. In tests, make the fake source return b'...' chunks (e.g. io.BytesIO(b'audio')) so read() yields bytes.

Example fix

// before
source = open(upload_path, 'r')  # text mode -> str chunks
safe_name, temp_path, size = stage_bgm_upload(source, upload.filename)

// after
source = open(upload_path, 'rb')  # binary mode -> bytes chunks
safe_name, temp_path, size = stage_bgm_upload(source, upload.filename)
Defensive patterns

Strategy: type-guard

Validate before calling

def is_binary_source(source) -> bool:
    probe = source.read(0)
    source.seek(0) if hasattr(source, 'seek') else None
    return isinstance(probe, (bytes, bytearray, memoryview))

Type guard

from typing import BinaryIO

def assert_binary_upload(source) -> None:
    if not isinstance(source, (io.RawIOBase, io.BufferedIOBase)) and not hasattr(source, 'read'):
        raise TypeError('upload source must be a binary file object')
    sample = source.read(1)
    if isinstance(sample, str):
        raise TypeError('upload source is text-mode; reopen in \'rb\'')
    if hasattr(source, 'seek'):
        source.seek(0)

Try / catch

try:
    stage_bgm_upload(source, filename)
except BgmUploadError as e:
    if 'must be binary' in str(e):
        source = io.BytesIO(original_bytes)  # reopen as binary and retry once

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of harry0703/MoneyPrinterTurbo@1f9f19c202 (2026-08-14). Data as JSON: /api/errors/2392b12130961689. Report an issue: GitHub.