Compare commits

...

1 Commits

Author SHA1 Message Date
Tim Lingo f6aa072d5f fix(el-runtime/win): stop truncating HTTP responses to the last fs_read length
The Windows el runtime stashes the byte count of the last fs_read in a
thread-global (_tl_fs_read_len) so binary files (PNG/WOFF2) serve without
strlen truncation at embedded NULs. But it was consumed unconditionally:
any handler that fs_reads a file and then returns a DIFFERENT, longer
response (e.g. /api/safety-contact reads the 178-byte contact file, then
wraps it in {"configured":true,"contact":...,"ok":true}) had its reply
clipped to the file size — the client received truncated, invalid JSON.

Record the buffer the length belongs to (_tl_fs_read_ptr) and only trust the
byte count when the handler returns that exact buffer; otherwise measure the
built response with strlen. Fixes every fs_read-then-transform endpoint
(safety-contact GET/POST, etc.). macOS (native runtime) was unaffected.

Verified root cause on Windows 11: GET/POST /api/safety-contact returned
Content-Length 178 with a body truncated mid-JSON; macOS returned the full
208-byte body. This is why the Neuron Windows app could not save the safety
contact (and would fail most soul calls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:12:31 -05:00
+19 -4
View File
@@ -81,8 +81,13 @@ static _Thread_local ElArena _tl_arena = {NULL, 0, 0};
static _Thread_local int _tl_arena_active = 0;
/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response.
* Allows serving PNGs and other binary files without strlen truncation. */
* Allows serving PNGs and other binary files without strlen truncation.
* _tl_fs_read_ptr records WHICH buffer that length belongs to: the length is only
* valid when the handler returns that exact buffer (a raw file). A handler that
* fs_reads then builds a different/longer response (e.g. wraps a file in JSON) must
* be measured by strlen otherwise the reply is truncated to the file's size. */
static _Thread_local size_t _tl_fs_read_len = 0;
static _Thread_local const char* _tl_fs_read_ptr = NULL;
static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return;
@@ -1558,11 +1563,15 @@ static void* http_worker(void* arg) {
const char* rs = EL_CSTR(r);
/* Copy response out BEFORE arena teardown.
* For binary files, _tl_fs_read_len holds the real byte count
* use memcpy instead of strdup so null bytes are preserved. */
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
* use memcpy instead of strdup so null bytes are preserved. But only
* trust that count when the handler returned the fs_read buffer itself;
* a wrapped/concatenated response must be measured by strlen. */
int is_fs_body = (_tl_fs_read_len > 0 && rs == _tl_fs_read_ptr);
size_t rlen = is_fs_body ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; }
_tl_fs_read_len = is_fs_body ? rlen : 0; /* length http_send_response should use */
} else {
response = el_strdup_persist("el-runtime: no http handler registered");
}
@@ -1806,10 +1815,14 @@ static void* http_worker_v2(void* arg) {
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
const char* rs = EL_CSTR(r);
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
/* Only trust fs_read's byte count when the handler returned that exact
* buffer (raw binary file); otherwise measure the built response by strlen. */
int is_fs_body = (_tl_fs_read_len > 0 && rs == _tl_fs_read_ptr);
size_t rlen = is_fs_body ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; }
_tl_fs_read_len = is_fs_body ? rlen : 0; /* length http_send_response should use */
el_release(hmap);
} else {
response = el_strdup_persist(
@@ -1926,6 +1939,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
el_val_t fs_read(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
_tl_fs_read_len = 0;
_tl_fs_read_ptr = NULL;
if (!path) return el_wrap_str(el_strdup(""));
FILE* f = fopen(path, "rb");
if (!f) return el_wrap_str(el_strdup(""));
@@ -1937,6 +1951,7 @@ el_val_t fs_read(el_val_t pathv) {
size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0';
_tl_fs_read_len = got; /* store real byte count for binary-safe send */
_tl_fs_read_ptr = buf; /* ...valid only if THIS buffer is the response body */
fclose(f);
return el_wrap_str(buf);
}