From 15a3243934f384c8b05f865963cc4a04fc9e271f Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sun, 22 Feb 2026 22:37:46 +0100 Subject: [PATCH] Fix ASGI headers to use bytes instead of str 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 --- CHANGELOG.md | 14 +++++++ c_src/py_asgi.c | 86 +++++++++++++++++++++++++++++++++++++++ src/erlang_python.app.src | 2 +- 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16051a5..4f8a43b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 1.6.1 (2026-02-22) + +### Fixed + +- **ASGI headers now correctly use bytes instead of str** - Fixed ASGI spec compliance + issue where headers were being converted to Python `str` objects instead of `bytes`. + The ASGI specification requires headers to be `list[tuple[bytes, bytes]]`. This was + causing authentication failures and form parsing issues with frameworks like Starlette + and FastAPI, which search for headers using bytes keys (e.g., `b"content-type"`). + - Added explicit header handling in `asgi_scope_from_map()` to bypass generic conversion + - Headers are now correctly converted using `PyBytes_FromStringAndSize()` + - Supports both list `[name, value]` and tuple `{name, value}` header formats from Erlang + - Fixes GitHub issue #1 + ## 1.6.0 (2026-02-22) ### Added diff --git a/c_src/py_asgi.c b/c_src/py_asgi.c index aedbb41..2a41f72 100644 --- a/c_src/py_asgi.c +++ b/c_src/py_asgi.c @@ -1099,6 +1099,92 @@ static PyObject *asgi_scope_from_map(ErlNifEnv *env, ERL_NIF_TERM scope_map) { Py_INCREF(ASGI_EMPTY_BYTES); py_value = ASGI_EMPTY_BYTES; } + } else if (py_key == ASGI_KEY_HEADERS) { + /* + * ASGI spec requires headers to be list[tuple[bytes, bytes]]. + * The Erlang representation is a list of [name_binary, value_binary] pairs. + * We must convert binaries to Python bytes (not str) for ASGI compliance. + */ + unsigned int headers_len; + if (enif_get_list_length(env, value, &headers_len)) { + py_value = PyList_New(headers_len); + if (py_value == NULL) { + if (!key_borrowed) { + Py_DECREF(py_key); + } + enif_map_iterator_destroy(env, &iter); + Py_DECREF(scope); + return NULL; + } + + ERL_NIF_TERM head, tail = value; + for (unsigned int idx = 0; idx < headers_len; idx++) { + if (!enif_get_list_cell(env, tail, &head, &tail)) { + Py_DECREF(py_value); + py_value = NULL; + break; + } + + /* Each header is a 2-element list [name, value] or tuple {name, value} */ + ERL_NIF_TERM hname_term, hvalue_term; + int harity; + const ERL_NIF_TERM *htuple; + ERL_NIF_TERM hhead, htail; + + if (enif_get_tuple(env, head, &harity, &htuple) && harity == 2) { + /* Tuple format: {name, value} */ + hname_term = htuple[0]; + hvalue_term = htuple[1]; + } else if (enif_get_list_cell(env, head, &hhead, &htail)) { + /* List format: [name, value] */ + hname_term = hhead; + if (!enif_get_list_cell(env, htail, &hvalue_term, &htail)) { + Py_DECREF(py_value); + py_value = NULL; + break; + } + } else { + Py_DECREF(py_value); + py_value = NULL; + break; + } + + /* Extract binaries and convert to Python bytes */ + ErlNifBinary name_bin, value_bin; + if (!enif_inspect_binary(env, hname_term, &name_bin) || + !enif_inspect_binary(env, hvalue_term, &value_bin)) { + Py_DECREF(py_value); + py_value = NULL; + break; + } + + /* Create tuple(bytes, bytes) per ASGI spec */ + PyObject *py_name = PyBytes_FromStringAndSize( + (char *)name_bin.data, name_bin.size); + PyObject *py_hvalue = PyBytes_FromStringAndSize( + (char *)value_bin.data, value_bin.size); + + if (py_name == NULL || py_hvalue == NULL) { + Py_XDECREF(py_name); + Py_XDECREF(py_hvalue); + Py_DECREF(py_value); + py_value = NULL; + break; + } + + PyObject *header_tuple = PyTuple_Pack(2, py_name, py_hvalue); + Py_DECREF(py_name); + Py_DECREF(py_hvalue); + + if (header_tuple == NULL) { + Py_DECREF(py_value); + py_value = NULL; + break; + } + + PyList_SET_ITEM(py_value, idx, header_tuple); /* Steals reference */ + } + } } /* Generic conversion if no optimization applied */ diff --git a/src/erlang_python.app.src b/src/erlang_python.app.src index 4fc6073..7748983 100644 --- a/src/erlang_python.app.src +++ b/src/erlang_python.app.src @@ -1,6 +1,6 @@ {application, erlang_python, [ {description, "Execute Python applications from Erlang using dirty NIFs"}, - {vsn, "1.6.0"}, + {vsn, "1.6.1"}, {registered, [py_pool]}, {mod, {erlang_python_app, []}}, {applications, [