aio-libs/aiohttp · error · ValueError

Separator should be at least one-byte string

Error message

Separator should be at least one-byte string

What it means

Raised by StreamReader.readuntil when the separator argument is an empty byte string (length 0). An empty separator would match at every position and never terminate the read, so aiohttp requires at least one byte. The default separator is b'\n'.

Solutions

  1. Provide a non-empty separator, e.g. await stream.readuntil(b'\r\n').
  2. Validate the separator before calling: if not separator: raise ValueError(...).
  3. Use the default readline() (newline-delimited) when you just want line-by-line reading.

Example fix

# before
sep = boundary.encode()  # boundary was ''
line = await stream.readuntil(sep)

# after
if not boundary:
    raise ValueError('boundary must not be empty')
line = await stream.readuntil(boundary.encode())
Defensive patterns

Strategy: validation

Validate before calling

if not separator:
    raise ValueError('separator must be non-empty')
line = await stream.readuntil(separator)

Type guard

def valid_separator(sep) -> bool:
    return isinstance(sep, (bytes, bytearray)) and len(sep) >= 1

Try / catch

try:
    line = await stream.readuntil(separator)
except ValueError as e:
    if 'Separator' in str(e):
        line = await stream.readuntil(b'\n')
    else:
        raise

Prevention

When it happens

Trigger: Calling await stream.readuntil(b'') explicitly; passing a separator computed from user input or a header that resolved to empty bytes; calling readline()/readuntil() with a variable that happened to be b''.

Common situations: Parsing a delimiter from configuration where the delimiter was unset; splitting on a boundary string that turned out empty; passing None and converting with bytes(None-ish).

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/f5694f28c7ed0fcd. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/streams.py:386

            self._waiter = None

    async def _fire_chunk_received(self, chunk: bytes) -> None:
        cb = self._on_chunk_received
        assert cb is not None
        # Run under the same per-stream timer that _wait() uses, so a hung
        # trace handler is bounded by sock_read just like a hung socket read would be.
        with self._timer:
            await cb(chunk)

    async def readline(self, *, max_line_length: int | None = None) -> bytes:
        return await self.readuntil(max_size=max_line_length)

    async def readuntil(
        self, separator: bytes = b"\n", *, max_size: int | None = None
    ) -> bytes:
        seplen = len(separator)
        if seplen == 0:
            raise ValueError("Separator should be at least one-byte string")

        if self._exception is not None:
            raise self._exception

        chunk = b""
        chunk_size = 0
        not_enough = True
        max_size = max_size or self._high_water

        while not_enough:
            while self._buffer and not_enough:
                offset = self._buffer_offset
                ichar = self._buffer[0].find(separator, offset) + 1
                # Read from current offset to found separator or to the end.
                data = self._read_nowait_chunk(
                    ichar - offset + seplen - 1 if ichar else -1
                )
                chunk += data

View on GitHub (pinned to d041d4d0fd)