Repository: apify/crawlee-python
File: src/crawlee/browsers/_playwright_browser_controller.py (Python)

Code before:
        await self._browser.close()

    def _on_page_close(self, page: Page) -> None:
        """Handle actions after a page is closed."""
        self._pages.remove(page)

    async def _create_browser_context(
        self, page_options: Mapping[str, Any] | None = None, proxy_info: ProxyInfo | None = None
    ) -> BrowserContext:
        """Create a new browser context with the specified proxy settings."""
        if self._header_generator:
            common_headers = self._header_generator.get_common_headers()
            sec_ch_ua_headers = self._header_generator.get_sec_ch_ua_headers(browser_type=self.browser_type)
            user_agent_header = self._header_generator.get_user_agent_header(browser_type=self.browser_type)
            extra_http_headers = dict(common_headers | sec_ch_ua_headers | user_agent_header)
        else:
            extra_http_headers = None

        page_options = dict(page_options) if page_options else {}
        page_options['extra_http_headers'] = page_options.get('extra_http_headers', extra_http_headers)

        proxy = (
            ProxySettings(
                server=f'{proxy_info.scheme}://{proxy_info.hostname}:{proxy_info.port}',
                username=proxy_info.username,
                password=proxy_info.password,
            )
            if proxy_info
            else None
        )

        return await self._browser.new_context(proxy=proxy, **page_options)


Review comment (suggestion):
Done as recommended in PW documentation.

From: [browser_context.close](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-close)
"Closes the browser context. All the pages that belong to the browser context will be closed."

From: [browser.new_context]( https://playwright.dev/python/docs/api/class-browser#browser-new-context)
"If directly using this method to create [BrowserContext](https://playwright.dev/python/docs/api/class-browsercontext)s, it is best practice to explicitly close the returned context via [browser_context.close()](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-close) when your code is done with the [BrowserContext](https://playwright.dev/python/docs/api/class-browsercontext), and before calling [browser.close()](https://playwright.dev/python/docs/api/class-browser#browser-close). This will ensure the context is closed gracefully and any artifacts—like HARs and videos—are fully flushed and saved."

From: [browser.close](https://playwright.dev/python/docs/api/class-browser#browser-close)
"This is similar to force quitting the browser. Therefore, you should call [browser_context.close()](https://playwright.dev/python/docs/api/class-browsercontext#browser-context-close) on any [BrowserContext](https://playwright.dev/python/docs/api/class-browsercontext)'s you explicitly created earlier with [browser.new_context()](https://playwright.dev/python/docs/api/class-browser#browser-new-context) before calling [browser.close()](https://playwright.dev/python/docs/api/class-browser#browser-close)."

Code after:
        page = await self._browser_context.new_page()

        # Handle page close event
        page.on(event='close', f=self._on_page_close)

        # Update internal state
        self._pages.append(page)
        self._last_page_opened_at = datetime.now(timezone.utc)

        return page

    @override
    async def close(self, *, force: bool = False) -> None:
        """Close the browser.

        Args:
            force: Whether to force close all open pages before closing the browser.

        Raises:
            ValueError: If there are still open pages when trying to close the browser.
        """
        if self.pages_count > 0 and not force:
            raise ValueError('Cannot close the browser while there are open pages.')

        if self._browser_context:
            await self._browser_context.close()
        await self._browser.close()

    def _on_page_close(self, page: Page) -> None:
        """Handle actions after a page is closed."""
        self._pages.remove(page)

    async def _create_browser_context(
        self, browser_new_context_options: Mapping[str, Any] | None = None, proxy_info: ProxyInfo | None = None
    ) -> BrowserContext:
        """Create a new browser context with the specified proxy settings."""
        if self._header_generator:
            common_headers = self._header_generator.get_common_headers()
            sec_ch_ua_headers = self._header_generator.get_sec_ch_ua_headers(browser_type=self.browser_type)
            user_agent_header = self._header_generator.get_user_agent_header(browser_type=self.browser_type)
            extra_http_headers = dict(common_headers | sec_ch_ua_headers | user_agent_header)
        else:
            extra_http_headers = None

        browser_new_context_options = dict(browser_new_context_options) if browser_new_context_options else {}
        browser_new_context_options['extra_http_headers'] = browser_new_context_options.get(
            'extra_http_headers', extra_http_headers
        )

        if proxy_info:
            if browser_new_context_options['proxy']: