Skip to content

Commit 7326786

Browse files
authored
Fix ASGI headers to use bytes instead of str (#4)
The ASGI specification requires headers to be list[tuple[bytes, bytes]]. The asgi_scope_from_map() function was falling through to generic conversion which decoded Erlang binaries as Python str instead of bytes. This caused authentication failures and form parsing issues with frameworks like Starlette and FastAPI, which search for headers using bytes keys (e.g., b"content-type"). Changes: - Add explicit header handling in asgi_scope_from_map() - Convert header names and values using PyBytes_FromStringAndSize() - Support both list [name, value] and tuple {name, value} formats - Bump version to 1.6.1 Fixes #1
1 parent 44b4fcb commit 7326786

File tree

3 files changed

+101
-1
lines changed

3 files changed

+101
-1
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 1.6.1 (2026-02-22)
4+
5+
### Fixed
6+
7+
- **ASGI headers now correctly use bytes instead of str** - Fixed ASGI spec compliance
8+
issue where headers were being converted to Python `str` objects instead of `bytes`.
9+
The ASGI specification requires headers to be `list[tuple[bytes, bytes]]`. This was
10+
causing authentication failures and form parsing issues with frameworks like Starlette
11+
and FastAPI, which search for headers using bytes keys (e.g., `b"content-type"`).
12+
- Added explicit header handling in `asgi_scope_from_map()` to bypass generic conversion
13+
- Headers are now correctly converted using `PyBytes_FromStringAndSize()`
14+
- Supports both list `[name, value]` and tuple `{name, value}` header formats from Erlang
15+
- Fixes GitHub issue #1
16+
317
## 1.6.0 (2026-02-22)
418

519
### Added

c_src/py_asgi.c

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,92 @@ static PyObject *asgi_scope_from_map(ErlNifEnv *env, ERL_NIF_TERM scope_map) {
10991099
Py_INCREF(ASGI_EMPTY_BYTES);
11001100
py_value = ASGI_EMPTY_BYTES;
11011101
}
1102+
} else if (py_key == ASGI_KEY_HEADERS) {
1103+
/*
1104+
* ASGI spec requires headers to be list[tuple[bytes, bytes]].
1105+
* The Erlang representation is a list of [name_binary, value_binary] pairs.
1106+
* We must convert binaries to Python bytes (not str) for ASGI compliance.
1107+
*/
1108+
unsigned int headers_len;
1109+
if (enif_get_list_length(env, value, &headers_len)) {
1110+
py_value = PyList_New(headers_len);
1111+
if (py_value == NULL) {
1112+
if (!key_borrowed) {
1113+
Py_DECREF(py_key);
1114+
}
1115+
enif_map_iterator_destroy(env, &iter);
1116+
Py_DECREF(scope);
1117+
return NULL;
1118+
}
1119+
1120+
ERL_NIF_TERM head, tail = value;
1121+
for (unsigned int idx = 0; idx < headers_len; idx++) {
1122+
if (!enif_get_list_cell(env, tail, &head, &tail)) {
1123+
Py_DECREF(py_value);
1124+
py_value = NULL;
1125+
break;
1126+
}
1127+
1128+
/* Each header is a 2-element list [name, value] or tuple {name, value} */
1129+
ERL_NIF_TERM hname_term, hvalue_term;
1130+
int harity;
1131+
const ERL_NIF_TERM *htuple;
1132+
ERL_NIF_TERM hhead, htail;
1133+
1134+
if (enif_get_tuple(env, head, &harity, &htuple) && harity == 2) {
1135+
/* Tuple format: {name, value} */
1136+
hname_term = htuple[0];
1137+
hvalue_term = htuple[1];
1138+
} else if (enif_get_list_cell(env, head, &hhead, &htail)) {
1139+
/* List format: [name, value] */
1140+
hname_term = hhead;
1141+
if (!enif_get_list_cell(env, htail, &hvalue_term, &htail)) {
1142+
Py_DECREF(py_value);
1143+
py_value = NULL;
1144+
break;
1145+
}
1146+
} else {
1147+
Py_DECREF(py_value);
1148+
py_value = NULL;
1149+
break;
1150+
}
1151+
1152+
/* Extract binaries and convert to Python bytes */
1153+
ErlNifBinary name_bin, value_bin;
1154+
if (!enif_inspect_binary(env, hname_term, &name_bin) ||
1155+
!enif_inspect_binary(env, hvalue_term, &value_bin)) {
1156+
Py_DECREF(py_value);
1157+
py_value = NULL;
1158+
break;
1159+
}
1160+
1161+
/* Create tuple(bytes, bytes) per ASGI spec */
1162+
PyObject *py_name = PyBytes_FromStringAndSize(
1163+
(char *)name_bin.data, name_bin.size);
1164+
PyObject *py_hvalue = PyBytes_FromStringAndSize(
1165+
(char *)value_bin.data, value_bin.size);
1166+
1167+
if (py_name == NULL || py_hvalue == NULL) {
1168+
Py_XDECREF(py_name);
1169+
Py_XDECREF(py_hvalue);
1170+
Py_DECREF(py_value);
1171+
py_value = NULL;
1172+
break;
1173+
}
1174+
1175+
PyObject *header_tuple = PyTuple_Pack(2, py_name, py_hvalue);
1176+
Py_DECREF(py_name);
1177+
Py_DECREF(py_hvalue);
1178+
1179+
if (header_tuple == NULL) {
1180+
Py_DECREF(py_value);
1181+
py_value = NULL;
1182+
break;
1183+
}
1184+
1185+
PyList_SET_ITEM(py_value, idx, header_tuple); /* Steals reference */
1186+
}
1187+
}
11021188
}
11031189

11041190
/* Generic conversion if no optimization applied */

src/erlang_python.app.src

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{application, erlang_python, [
22
{description, "Execute Python applications from Erlang using dirty NIFs"},
3-
{vsn, "1.6.0"},
3+
{vsn, "1.6.1"},
44
{registered, [py_pool]},
55
{mod, {erlang_python_app, []}},
66
{applications, [

0 commit comments

Comments
 (0)