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.