Skip to content

Commit f72bd37

Browse files
linesightclaude
andcommitted
feat(helpers): add cef.UnraisableHook for exceptions escaping handlers
Cython 3 callback handlers are declared "noexcept", so an exception that escapes a handler (one not wrapped in try/except, or whose except block itself raises) does not propagate into CEF's C++ code. Python instead reports it via sys.unraisablehook, which by default only prints to stderr and is easy to miss. Add a UnraisableHook helper, companion to ExceptHook, that forwards the escaped exception to ExceptHook so it is written to error.log, printed, and CEF is shut down cleanly - same as any other Python exception. Wire it into the tutorial.py example (sys.unraisablehook = cef.UnraisableHook) and document it in Tutorial.md and cefpython.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 073982f commit f72bd37

5 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎api/API-index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
* [SetGlobalClientHandler](cefpython.md#setglobalclienthandler)
163163
* [SetOsModalLoop](cefpython.md#setosmodalloop)
164164
* [Shutdown](cefpython.md#shutdown)
165+
* [UnraisableHook](cefpython.md#unraisablehook)
165166
* [Command line switches](CommandLineSwitches.md#command-line-switches)
166167
* [enable-media-stream](CommandLineSwitches.md#enable-media-stream)
167168
* [proxy-server](CommandLineSwitches.md#proxy-server)

‎api/cefpython.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Table of contents:
3232
* [SetGlobalClientHandler](#setglobalclienthandler)
3333
* [SetOsModalLoop](#setosmodalloop)
3434
* [Shutdown](#shutdown)
35+
* [UnraisableHook](#unraisablehook)
3536

3637

3738
## Functions
@@ -93,6 +94,9 @@ This hook does the following: in case of exception write it to
9394
the "error.log" file, display it to the console, shutdown CEF
9495
and exit application immediately by ignoring "finally" (_exit()).
9596

97+
See also [UnraisableHook](#unraisablehook) for exceptions that escape a
98+
callback handler.
99+
96100
See also Tutorial: [Handling Python exceptions](../docs/Tutorial.md#handling-python-exceptions).
97101

98102

@@ -380,3 +384,31 @@ modal message loop. Set to false after exiting the modal message loop.
380384
This function should be called on the main application thread (UI thread) to shut down CEF before the application exits.
381385

382386
You must call this function so that CEF shuts down cleanly. Remember also to delete all CEF browsers references for the browsers to shut down cleanly. For an example see the wxpython.py example MainFrame.OnClose().
387+
388+
389+
### UnraisableHook
390+
391+
| Parameter | Type |
392+
| --- | --- |
393+
| unraisable | - |
394+
| __Return__ | void |
395+
396+
Global unraisable hook, companion to [ExceptHook](#excepthook). Assign it
397+
with `sys.unraisablehook = cef.UnraisableHook`.
398+
399+
CEF Python's callback handlers are compiled with Cython and declared as
400+
"noexcept", so an exception must not propagate out of a handler into CEF's
401+
C++ code. Handlers catch their own exceptions and forward them to
402+
`sys.excepthook`, but if an exception still escapes a handler (for example a
403+
handler not wrapped in try/except, or whose except block itself raises) then
404+
Python reports it through
405+
[sys.unraisablehook](https://docs.python.org/3/library/sys.html#sys.unraisablehook)
406+
rather than crashing the process. By default that only prints to stderr and
407+
is easy to miss.
408+
409+
This hook forwards the escaped exception to [ExceptHook](#excepthook), so it
410+
is written to the "error.log" file, printed, and CEF is shut down cleanly -
411+
same as any other Python exception. Reaching this code path indicates a bug
412+
in a handler that should be fixed.
413+
414+
See also Tutorial: [Handling Python exceptions](../docs/Tutorial.md#handling-python-exceptions).

‎docs/Tutorial.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,28 @@ The cef.ExceptHook helper function does the following:
163163
If you would like to modify `ExceptHook` behavior, see its source code
164164
in src/[helpers.pyx](../src/helpers.pyx) file.
165165

166+
There is a second, related case. CEF Python's callback handlers are
167+
compiled with Cython and declared as "noexcept", meaning an exception
168+
must not propagate out of a handler into CEF's C++ code. Handlers catch
169+
their own exceptions and forward them to `sys.excepthook`, but if an
170+
exception still escapes a handler (for example a handler that forgot to
171+
use try/except, or whose except block itself raises) then Python does
172+
not crash the process - it reports the exception through
173+
[sys.unraisablehook](https://docs.python.org/3/library/sys.html#sys.unraisablehook),
174+
which by default only prints to stderr and is easy to miss. To give
175+
these escaped exceptions the same treatment as any other error, examples
176+
also set:
177+
178+
```python
179+
sys.unraisablehook = cef.UnraisableHook # For exceptions that escape a handler
180+
```
181+
182+
cef.[UnraisableHook](../api/cefpython.md#unraisablehook) forwards to
183+
cef.[ExceptHook](../api/cefpython.md#excepthook), so an escaped exception
184+
is written to "error.log", printed, and CEF is shut down cleanly instead
185+
of being silently logged. Reaching this code path indicates a bug in a
186+
handler that should be fixed.
187+
166188

167189
## Settings
168190

‎examples/tutorial.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
def main():
6161
check_versions()
6262
sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error
63+
sys.unraisablehook = cef.UnraisableHook # Same, for exceptions that
64+
# escape a callback handler
6365
# To change user agent use either "product_version"
6466
# or "user_agent" options. Explained in Tutorial in
6567
# "Change user agent string" section.

‎src/helpers.pyx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ def ExceptHook(exc_type, exc_value, exc_trace):
4747
os._exit(1)
4848

4949

50+
def UnraisableHook(unraisable):
51+
"""Global unraisable hook, companion to ExceptHook.
52+
53+
Cython callback handlers are declared "noexcept": an exception that
54+
escapes a handler (one not wrapped in try/except, or whose except block
55+
itself raises) does not propagate into CEF's C++ code. Python instead
56+
reports it via sys.unraisablehook, which by default only prints to stderr
57+
and is easy to miss. Assigning this hook routes such exceptions through
58+
ExceptHook so they are written to "error.log", printed, and CEF is shut
59+
down cleanly - same as any other Python exception.
60+
"""
61+
ExceptHook(unraisable.exc_type, unraisable.exc_value,
62+
unraisable.exc_traceback)
63+
64+
5065
cpdef str GetModuleDirectory():
5166
"""Get path to the cefpython module (so/pyd)."""
5267
if platform.system() == "Linux" and os.getenv("CEFPYTHON3_PATH"):

0 commit comments

Comments
 (0)