Skip to content

context.request.user_data mutations are silently dropped on retry #2061

Description

@vdusek

Description

Mutating context.request.user_data inside a handler doesn't persist across a retry. The change is visible within the same handler run, but silently gone on the next attempt.

Cause: context.request is an isolated deepcopy created for handler execution (RequestHandlerRunResult, src/crawlee/_types.py:277), while the retry reclaims the original request. So in-handler mutations to context.request (including user_data) are dropped on retry. This differs from Crawlee for JS, where request.userData mutations persist across retries.

Surfaced in discussion #2028, where the natural workaround (stash state on request.user_data, read it on the retry) silently fails.

Reproduction

import asyncio

from crawlee.crawlers import BasicCrawler, BasicCrawlingContext


async def main() -> None:
    crawler = BasicCrawler(max_request_retries=3)

    @crawler.router.default_handler
    async def handler(context: BasicCrawlingContext) -> None:
        print(f'retry_count={context.request.retry_count}, flag={context.request.user_data.get("flag")!r}')
        if context.request.retry_count == 0:
            context.request.user_data['flag'] = 'set-on-first-attempt'
            raise RuntimeError('force a retry')

    await crawler.run(['https://example.com/'])


asyncio.run(main())

Actual

retry_count=0, flag=None
retry_count=1, flag=None

Expected

retry_count=0, flag=None
retry_count=1, flag='set-on-first-attempt'

Reproduced on crawlee 1.8.x.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working.t-toolingIssues with this label are in the ownership of the tooling team.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions