fix(spiders): preserve cookies for browser-engine responses in cache - #379
Conversation
…4Vinci#376) ResponseCacheManager.put() only recognized the flat `dict` shape that static-engine responses use for `Response.cookies`. Browser engines (Playwright) populate cookies as a `tuple` of full cookie dicts, so the isinstance(dict) guard fell through to the `else {}` branch and silently discarded every cookie before it ever reached the cache file. Replaying a cached browser-engine response then rebuilt it with cookies={}. Preserve whichever shape the cookies are in instead of collapsing anything non-dict to {}: serialize a tuple as a JSON array and a dict as a JSON object, then restore the tuple shape on read since JSON arrays deserialize back as `list`. Add regression tests covering both the browser-engine (tuple) and static-engine (dict) cookie round-trip through put()/get().
|
Reviewed and verified this locally against dev. LGTM. The root cause is as described, the new browser cookie test fails before the fix and passes after, and the full test suites, lint, and type checks are all clean. Edge cases the tests don't cover (empty tuple cookies, old cache files, float expiries) also behave. Minor notes, none blocking: passing Shape-preserving is the right call over flattening, which would collide keys and lose data. |
|
Thanks for the thorough verification — especially exercising the edge cases beyond the tests (empty tuple cookies, old cache files, float expiries). Agreed on all three notes: keeping the explicit conversion for readability, no schema machinery for a fix this narrow, and the |
|
LGTM, thanks for solving this! |
Fixes #376
Root cause
Response.cookieshas two possible shapes (scrapling/engines/toolbelt/custom.py:48):dict(convertor.py:317)tupleof full cookie dicts (convertor.py:139,convertor.py:287)ResponseCacheManager.put()only handled thedictcase:Any browser-engine response has
tuplecookies, soisinstance(..., dict)isFalseand theelse {}branch discards every cookie before it's ever written to the cache file.get()then rebuilds theResponsewithcookies={}, so a replayed browser-engine response has no cookies at all — this silently breaks anything downstream relying on cookies (sessions, auth, CSRF tokens).Fix
Preserve whichever shape the cookies are in, instead of collapsing non-dict cookies to
{}:put(): serialize atupleas a JSON array (list(response.cookies)) and adictas a JSON object (dict(response.cookies)).get(): JSON arrays deserialize back aslist, so restore thetupleshapeResponse.__init__expects; JSON objects already come back asdict.This keeps the round-trip shape-preserving rather than normalizing everything to a flat dict, so code that depends on the tuple-of-full-cookie-dicts shape for browser-engine responses (domain, path, expiry, etc.) still gets that shape back after a cache replay.
Tests
Added two regression tests to
tests/spiders/test_cache.py:test_put_get_roundtrip_preserves_browser_engine_cookies— tuple-of-dicts cookies (the browser-engine shape) surviveput()/get()unchanged. This fails onmain/devbefore the fix (assert {} == (...)) and passes after.test_put_get_roundtrip_preserves_static_engine_cookies— flat-dict cookies (the static-engine shape) still round-trip correctly, confirming no regression for the existing working case.Full
tests/spiders/test_cache.pysuite (12 tests) passes.ruff check,ruff format --check,mypy,pyright, andbanditall pass clean on the changed file. Ran the broader non-browser test suite (pytest tests/ -k "not (DynamicFetcher or StealthyFetcher)", 738 passed) to confirm no regressions elsewhere — the only failures were pre-existing, unrelated tests failing due to missing local Playwright browser binaries.Credit to @truongsontung for the initial diagnosis in the issue thread — I independently confirmed the same root cause and landed on a slightly different fix (shape-preserving round-trip rather than always flattening to a dict on read) to avoid changing the cookie shape browser-engine consumers see after a cache replay.