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, [