Skip to content

Commit dca7fa0

Browse files
linesightclaude
andcommitted
Handlers: document NULL-browser guards and log instead of silent return
Add why-comments and Debug() logging to the CefFrame::GetBrowser()==NULL guards across the V8 context, load, lifespan and cookie-access handlers so these skipped callbacks are diagnosable rather than silently dropped. - v8context: explain the async-IPC frame-detach case; document why bindings are re-sent on each new V8 context. - cookie filter: drop the incorrect issue cztomczak#676 attribution (that was the callbacks-not-called bug, fixed separately); document in RequestHandler.md that requests without a browser/frame cannot be filtered. - load handler: comment + log OnLoadStart/OnLoadEnd/OnLoadError. - lifespan: fix the OnBeforePopup comment (return false ALLOWS the popup, it does not cancel) and defer to CEF's default, matching the other guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ddb67b commit dca7fa0

5 files changed

Lines changed: 66 additions & 15 deletions

File tree

‎api/RequestHandler.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ Table of contents:
4646
Called on the IO thread before a cookie is sent with a network request.
4747
Return `True` to allow the cookie or `False` to block it.
4848

49+
Limitation: requests with no associated browser/frame (e.g. service workers,
50+
`CefURLRequest`, and some cross-origin subresource requests on the IO thread)
51+
cannot be filtered. The callback is not called for them and the cookie is
52+
allowed by default, matching CEF's own default.
53+
4954

5055
### CanSaveCookie
5156

@@ -61,6 +66,11 @@ Return `True` to allow the cookie or `False` to block it.
6166
Called on the IO thread before a cookie received from a network response is
6267
saved. Return `True` to allow the cookie or `False` to block it.
6368

69+
Limitation: requests with no associated browser/frame (e.g. service workers,
70+
`CefURLRequest`, and some cross-origin subresource requests on the IO thread)
71+
cannot be filtered. The callback is not called for them and the cookie is
72+
allowed by default, matching CEF's own default.
73+
6474

6575
### GetAuthCredentials
6676

‎src/handlers/cookie_access_filter.pyx‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ cdef public cpp_bool CookieAccessFilter_CanSendCookie(
2525
# browser was closed.
2626
if IsBrowserClosed(cef_browser):
2727
return False
28-
# Issue #676: This callback runs on the IO thread, where
29-
# CefFrame::GetBrowser() may return NULL (observed for cross-origin
30-
# subresource requests). CEF also documents browser/frame as optional
31-
# ("may be NULL for requests originating from service workers or
32-
# CefURLRequest", cef_resource_request_handler.h > CefCookieAccessFilter).
33-
# Without a resolvable browser we cannot build a PyFrame to dispatch on,
34-
# so log and allow the cookie by default (CEF's own default is true).
28+
# This callback runs on the IO thread, where CefFrame::GetBrowser()
29+
# may return NULL (observed for cross-origin subresource requests). CEF
30+
# also documents browser/frame as optional ("may be NULL for requests
31+
# originating from service workers or CefURLRequest",
32+
# cef_resource_request_handler.h > CefCookieAccessFilter). Without a
33+
# resolvable browser we cannot build a PyFrame to dispatch on, so log
34+
# and allow the cookie by default (CEF's own default is true). The app
35+
# therefore cannot filter these cookies - documented as a limitation in
36+
# api/RequestHandler.md.
3537
if not cef_frame.get() or not cef_frame.get().GetBrowser().get():
3638
Debug("CanSendCookie: no resolvable browser for the request"
3739
" (IO-thread / service-worker request); allowing by default")
@@ -75,10 +77,11 @@ cdef public cpp_bool CookieAccessFilter_CanSaveCookie(
7577
# browser was closed.
7678
if IsBrowserClosed(cef_browser):
7779
return False
78-
# Issue #676: see CanSendCookie above. On the IO thread
79-
# CefFrame::GetBrowser() may be NULL (and CEF documents browser/frame
80-
# as optional). Without a resolvable browser we cannot build a PyFrame,
81-
# so log and allow the cookie by default.
80+
# See CanSendCookie above. On the IO thread CefFrame::GetBrowser() may
81+
# be NULL (and CEF documents browser/frame as optional). Without a
82+
# resolvable browser we cannot build a PyFrame, so log and allow the
83+
# cookie by default; the app cannot filter these cookies (see
84+
# api/RequestHandler.md).
8285
if not cef_frame.get() or not cef_frame.get().GetBrowser().get():
8386
Debug("CanSaveCookie: no resolvable browser for the request"
8487
" (IO-thread / service-worker request); allowing by default")

‎src/handlers/lifespan_handler.pyx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,17 @@ cdef public cpp_bool LifespanHandler_OnBeforePopup(
4848
cdef object callback
4949
cdef py_bool returnValue
5050
try:
51+
# CefFrame::GetBrowser() can be NULL when the opener frame is detached
52+
# during a rapid lifecycle transition. Without a resolvable browser we
53+
# cannot build a PyFrame to pass to the user callback, so fall back to
54+
# CEF's default, consistent with the other NULL-browser guards. Note
55+
# that returning false ALLOWS the popup (CEF: return true cancels,
56+
# false allows - see cef_life_span_handler.h); this defers popup
57+
# handling to CEF rather than silently suppressing it.
5158
if not cefFrame.get().GetBrowser().get():
52-
return False # frame is being destroyed; cancel popup
59+
Debug("OnBeforePopup: frame has no browser (detached during"
60+
" lifecycle transition); deferring to CEF default")
61+
return False
5362
pyBrowser = GetPyBrowser(cefBrowser, "OnBeforePopup")
5463
pyFrame = GetPyFrame(cefFrame)
5564
pyTargetUrl = CefToPyString(targetUrl)

‎src/handlers/load_handler.pyx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ cdef public void LoadHandler_OnLoadStart(
3333
cdef PyFrame pyFrame
3434
cdef object clientCallback
3535
try:
36+
# A frame torn down mid-navigation (rapid create -> navigate -> destroy)
37+
# can already be detached by the time this UI-thread callback runs, in
38+
# which case CefFrame::GetBrowser() returns NULL. There is then no
39+
# browser to resolve a PyBrowser/PyFrame from and the load event refers
40+
# to a frame that no longer exists, so skipping is correct.
3641
if not cefFrame.get().GetBrowser().get():
42+
Debug("OnLoadStart: frame has no browser (detached during"
43+
" lifecycle transition), skipping")
3744
return
3845
pyBrowser = GetPyBrowser(cefBrowser, "OnLoadStart")
3946
pyFrame = GetPyFrame(cefFrame)
@@ -53,7 +60,11 @@ cdef public void LoadHandler_OnLoadEnd(
5360
cdef PyFrame pyFrame
5461
cdef object clientCallback
5562
try:
63+
# See OnLoadStart: a detached frame yields a NULL CefFrame::GetBrowser()
64+
# on the UI thread; nothing to dispatch on, so skip.
5665
if not cefFrame.get().GetBrowser().get():
66+
Debug("OnLoadEnd: frame has no browser (detached during"
67+
" lifecycle transition), skipping")
5768
return
5869
pyBrowser = GetPyBrowser(cefBrowser, "OnLoadEnd")
5970
pyFrame = GetPyFrame(cefFrame)
@@ -82,7 +93,11 @@ cdef public void LoadHandler_OnLoadError(
8293
# the error code will be ERR_ABORTED. In such cases calls
8394
# to OnLoadError should be ignored and not handled by user
8495
# scripts. The wxpython example implements such behavior.
96+
# See OnLoadStart: a detached frame yields a NULL CefFrame::GetBrowser()
97+
# on the UI thread; nothing to dispatch on, so skip.
8598
if not cefFrame.get().GetBrowser().get():
99+
Debug("OnLoadError: frame has no browser (detached during"
100+
" lifecycle transition), skipping")
86101
return
87102
pyBrowser = GetPyBrowser(cefBrowser, "OnLoadError")
88103
pyFrame = GetPyFrame(cefFrame)

‎src/handlers/v8context_handler.pyx‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,28 @@ cdef public void V8ContextHandler_OnContextCreated(
2222
cdef object clientCallback
2323
cdef JavascriptBindings jsBindings
2424
try:
25+
# These V8 events are reconstructed in the browser process from
26+
# asynchronous IPC messages sent by the renderer (see note at the top
27+
# of this file), so a frame from a rapid create -> navigate -> destroy
28+
# sequence can already be detached by the time we run here, in which
29+
# case CefFrame::GetBrowser() returns NULL. There is then no browser to
30+
# bind to or dispatch a callback for, so skipping is the correct thing
31+
# to do.
2532
if not cefFrame.get().GetBrowser().get():
33+
Debug("OnContextCreated: frame has no browser (detached during"
34+
" lifecycle transition), skipping")
2635
return
2736
pyBrowser = GetPyBrowser(cefBrowser, "OnContextCreated")
2837
pyBrowser.SetUserData("__v8ContextCreated", True)
2938
pyFrame = GetPyFrame(cefFrame)
30-
# Re-send bindings before user callback so that any ExecuteJavascript
31-
# the user sends from OnContextCreated has globals already registered
32-
# when it arrives at the renderer (both travel the same IPC channel).
39+
# Re-send the JS bindings to the renderer. OnContextCreated fires for
40+
# each new V8 context (page load, navigation, cross-origin navigation
41+
# under site isolation); a fresh context has none of the previously
42+
# injected globals, so they must be resent or bindings would be lost
43+
# after navigation. Do this before the user callback so that any
44+
# ExecuteJavascript the user issues from OnContextCreated finds its
45+
# globals already registered when it reaches the renderer (both travel
46+
# the same IPC channel).
3347
if pyFrame.IsMain():
3448
jsBindings = pyBrowser.GetJavascriptBindings()
3549
if jsBindings:

0 commit comments

Comments
 (0)