http.cookiejar's MozillaCookieJar Blames None on File Format (tested)

A non-blocking raw stream signals "no data ready yet" by returning None from readinto() -- distinct from the empty read that means real end-of-stream. We measured that loading a saved cookie jar runs straight into the already-confirmed TextIOWrapper.readline() collapse: a transient None becomes '' instead of raising, and MozillaCookieJar._really_load() reads exactly one line first to check a magic header -- if not NETSCAPE_MAGIC_RGX.match(f.readline()): raise LoadError(...). An empty first line fails that regex match every time, so the real, valid cookie file gets rejected with LoadError: '...' does not look like a Netscape format cookies file -- an error that blames the file's contents, when the file was never actually read.

1. A real, valid cookies.txt behind one transient None

import http.cookiejar, io

class FlakyRaw(io.RawIOBase):
    def __init__(self, payload):
        self.calls = 0
        self.sent = False
        self.payload = payload
    def readable(self):
        return True
    def readinto(self, b):
        self.calls += 1
        if self.calls == 1:
            return None            # transient: no data ready yet
        if not self.sent:
            self.sent = True
            n = len(self.payload)
            b[:n] = self.payload
            return n               # the real, complete cookie file
        return 0                   # real EOF after that

payload = (b"# Netscape HTTP Cookie File\n"
           b"example.com\tFALSE\t/\tFALSE\t2000000000\tname\tvalue\n")
raw = FlakyRaw(payload)
br = io.BufferedReader(raw)
tr = io.TextIOWrapper(br)

cj = http.cookiejar.MozillaCookieJar()
try:
    cj._really_load(tr, "cookies.txt", False, False)
    print("LOADED OK:", [(c.name, c.value) for c in cj])
except Exception as e:
    print("CRASHED:", type(e).__name__, "--", e)
CRASHED: LoadError -- 'cookies.txt' does not look like a Netscape format cookies file

Measured: the cookie file is valid and complete, with the correct # Netscape HTTP Cookie File header one read call away. TextIOWrapper.readline() silently collapsed the transient None into '', and that empty first line failed MozillaCookieJar's magic-header check -- producing an error that names the file's format as the problem, when no header line was actually read at all.

2. The fix: drain into a real buffer first, retrying on None

def read_retrying_on_none(buffered_reader, chunk_size=65536):
    out = io.BytesIO()
    while True:
        chunk = buffered_reader.read(chunk_size)
        if chunk is None:
            continue   # retry: not ready yet, not real EOF
        if not chunk:
            break      # real EOF
        out.write(chunk)
    out.seek(0)
    return out

raw2 = FlakyRaw(payload)
br2 = io.BufferedReader(raw2)
safe_buf = read_retrying_on_none(br2)
tr2 = io.TextIOWrapper(safe_buf)

cj2 = http.cookiejar.MozillaCookieJar()
cj2._really_load(tr2, "cookies.txt", False, False)
print("LOADED OK:", [(c.name, c.value) for c in cj2])
LOADED OK: [('name', 'value')]

Measured: resolving the None/EOF distinction at the binary layer before TextIOWrapper ever sees it loads the same file correctly. If MozillaCookieJar (or LWPCookieJar) rejects a cookie file you're confident is well-formed, the real cause is almost certainly a non-blocking stream behind it, not a corrupt or mistyped file.

Frequently asked questions

Is this documented behavior?

http.cookiejar's documentation describes load() as reading cookies from a file in the jar's native format; it says nothing about None or non-blocking streams. We measured that a None-collapsed-to-empty first line fails the magic-header regex check the same way a genuinely corrupt file would, with no way to tell the two apart from the exception alone.

Does LWPCookieJar have the same problem?

Yes -- we measured the identical mechanism: LWPCookieJar._really_load() also calls f.readline() first to check a magic header (a Set-Cookie3 (LWP) marker instead of the Netscape one), and an empty collapsed line fails that check the same way, raising LoadError: '...' does not look like a Set-Cookie3 (LWP) format file.

Why does this only affect the first line?

We measured that both classes check the magic header with a single readline() call before any loop begins. Every later line is read inside a while (line := f.readline()) != '': loop, where a None-collapsed-to-empty line would just look like normal end-of-file and stop the loop early -- silently dropping the rest of the cookies instead of raising, which is arguably worse since it gives no error at all.

Is this related to the other None/EOF findings?

Yes -- it's a direct real-world consequence of the already-confirmed TextIOWrapper.readline() None-to-empty-string collapse, applied to a specific, widely used consumer: persisted cookie jars loaded by requests and urllib.request.HTTPCookieProcessor. Same root mechanism as the misleading-content-blaming crashes already confirmed in gzip/bz2/lzma/tarfile/zipfile/ElementTree.parse(), just with its own file-format-specific wording.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in http.cookiejar.MozillaCookieJar (and confirmed the same mechanism on LWPCookieJar), loading a genuinely valid, complete Netscape-format cookie file through an io.TextIOWrapper over an io.BufferedReader over a custom non-blocking RawIOBase whose readinto() returns None once before returning the real bytes. PlainHeaders is our extension; Python's http.cookiejar and io modules are built in.