Skip to content

Commit 718755d

Browse files
committed
Update documentation for exception type breaking change
- README.md: Update error handling examples to use strings - docs/migration.md: Add security-related breaking changes section - c_src/py_nif.h: Update make_py_error documentation - c_src/py_logging.c: Use cached ATOM_OK/ATOM_ERROR for trace status - test/py_logging_SUITE.erl: Revert trace status to atoms Trace span status remains as atoms (ok/error) since these are known, bounded values that use cached atoms for efficiency.
1 parent 38ba654 commit 718755d

5 files changed

Lines changed: 49 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,10 @@ py:execution_mode(). %% => free_threaded | subinterp | multi_executor
558558
## Error Handling
559559

560560
```erlang
561-
{error, {'NameError', "name 'x' is not defined"}} = py:eval(<<"x">>).
562-
{error, {'ZeroDivisionError', "division by zero"}} = py:eval(<<"1/0">>).
561+
%% Python exceptions return {error, {TypeString, Message}}
562+
%% Note: Exception types are strings (not atoms) since v2.0 to prevent atom table exhaustion
563+
{error, {"NameError", "name 'x' is not defined"}} = py:eval(<<"x">>).
564+
{error, {"ZeroDivisionError", "division by zero"}} = py:eval(<<"1/0">>).
563565
{error, timeout} = py:eval(<<"sum(range(10**9))">>, #{}, 100).
564566
```
565567

‎c_src/py_logging.c‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,16 @@ static PyObject *erlang_trace_end_impl(PyObject *self, PyObject *args) {
277277
}
278278

279279
ERL_NIF_TERM span_id_term = enif_make_uint64(msg_env, span_id);
280-
/* Use string instead of atom to prevent atom table exhaustion from
281-
* arbitrary Python trace status strings */
282-
ERL_NIF_TERM status_term = enif_make_string(msg_env, status, ERL_NIF_LATIN1);
280+
/* Use cached atom values for status - safe since only "ok"/"error" are expected */
281+
ERL_NIF_TERM status_term;
282+
if (strcmp(status, "ok") == 0) {
283+
status_term = enif_make_copy(msg_env, ATOM_OK);
284+
} else if (strcmp(status, "error") == 0) {
285+
status_term = enif_make_copy(msg_env, ATOM_ERROR);
286+
} else {
287+
/* Unknown status - use string to be safe */
288+
status_term = enif_make_string(msg_env, status, ERL_NIF_LATIN1);
289+
}
283290
ERL_NIF_TERM attrs_term = py_to_term(msg_env, attrs);
284291

285292
uint64_t ts = get_monotonic_ns() / 1000;

‎c_src/py_nif.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,9 +1006,12 @@ static ERL_NIF_TERM make_error(ErlNifEnv *env, const char *reason);
10061006
* error tuple, and clears the Python error state.
10071007
*
10081008
* @param env NIF environment for term allocation
1009-
* @return `{error, {ExceptionType, Message}}` tuple
1009+
* @return `{error, {ExceptionTypeString, MessageString}}` tuple
1010+
* where ExceptionTypeString is the Python exception class name
1011+
* (e.g., "NameError", "TypeError") as a string (not atom)
10101012
*
10111013
* @note Always clears the Python exception state
1014+
* @note Exception type is returned as string to prevent atom table exhaustion
10121015
*/
10131016
static ERL_NIF_TERM make_py_error(ErlNifEnv *env);
10141017

‎docs/migration.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,35 @@ This guide covers breaking changes and migration steps when upgrading from erlan
1212
- [ ] Replace subprocess calls with Erlang ports
1313
- [ ] Move signal handlers to Erlang level
1414
- [ ] Review any `os.fork`/`os.exec` usage
15+
- [ ] Update error pattern matching: exception types are now strings
16+
17+
## Security-Related Breaking Changes
18+
19+
### Exception Types Changed from Atoms to Strings
20+
21+
**Reason:** Atoms are never garbage collected in Erlang. Using atoms for Python exception type names (which are unbounded due to custom exceptions and third-party libraries) could exhaust the atom table.
22+
23+
**Before (v1.8.x):**
24+
```erlang
25+
case py:eval(Code) of
26+
{ok, Result} -> Result;
27+
{error, {'NameError', Msg}} -> handle_name_error(Msg);
28+
{error, {'TypeError', Msg}} -> handle_type_error(Msg);
29+
{error, {ExcType, Msg}} -> handle_other(ExcType, Msg)
30+
end.
31+
```
32+
33+
**After (v2.0):**
34+
```erlang
35+
case py:eval(Code) of
36+
{ok, Result} -> Result;
37+
{error, {"NameError", Msg}} -> handle_name_error(Msg);
38+
{error, {"TypeError", Msg}} -> handle_type_error(Msg);
39+
{error, {ExcType, Msg}} -> handle_other(ExcType, Msg) %% ExcType is now a string
40+
end.
41+
```
42+
43+
**Note:** Trace span status remains as atoms (`ok` | `error`) since these are known, bounded values.
1544

1645
## API Changes
1746

‎test/py_logging_SUITE.erl‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ with erlang.Span('test-span', key='value'):
134134
1 = length(Spans),
135135
[Span] = Spans,
136136
<<"test-span">> = maps:get(name, Span),
137-
"ok" = maps:get(status, Span), %% Status is now string since v2.0
137+
ok = maps:get(status, Span),
138138
true = maps:get(duration_us, Span) >= 0,
139139
#{<<"key">> := <<"value">>} = maps:get(attributes, Span),
140140
ok.
@@ -222,7 +222,7 @@ except ValueError:
222222
">>),
223223

224224
{ok, [Span]} = py:get_traces(),
225-
"error" = maps:get(status, Span), %% Status is now string since v2.0
225+
error = maps:get(status, Span),
226226
EndAttrs = maps:get(end_attrs, Span),
227227
true = maps:is_key(<<"exception">>, EndAttrs),
228228
ok.

0 commit comments

Comments
 (0)