Fix truncated POST/GET /api/safety-contact response
Saving the 988 crisis-line contact returned truncated, unparseable JSON —
cut mid-"set_at" at the file's byte length (e.g. 178 of a 218-byte
response). The contact written to disk was complete; only the HTTP response
was clipped, so a real customer's crisis-contact save came back corrupt.
Root cause is in the el runtime's response writer, not a handler buffer:
fs_read stores the file's byte count in a thread-local (_tl_fs_read_len)
for binary-safe file serving, and the response writer uses that length when
non-zero instead of strlen(body) (el_runtime.c:1409). Both safety-contact
handlers call fs_read (the POST read-back verify; the GET file read) and
then return a LONGER wrapped JSON string, so the response is capped to the
file size.
Soul-source fix (no runtime change needed):
- POST: verify persistence via fs_write's return (1 = all bytes written)
instead of an fs_read read-back — removes the fs_read, so nothing caps the
response.
- GET: fs_read is required, so reset the thread-local after it with a no-op
fs_read("") (fs_read zeroes the length before it opens a path) so the
wrapped response is sent in full.
Verified: POST (crisis-line + custom) and GET now return complete, valid
JSON (parses cleanly, full contact incl. set_at). Regenerated dist/soul.c +
dist/safety.c (3GB RSS watchdog, release el_runtime v1.0.0-20260501).
Full suite still green: verify-soul-contract GATE PASS (PRESENCE +
IMMUTABILITY), genesis boot survives (/health 200, no segfault), bounded-
persona floor still compiled in.
NOTE: the underlying runtime leak (any handler that fs_reads then returns a
longer string) is worth a proper fix in el_runtime.c (use the max of
strlen and _tl_fs_read_len) so this class can't recur.
This commit is contained in:
+3
-3
@@ -340,6 +340,7 @@ el_val_t handle_safety_contact_get(void) {
|
||||
if (str_eq(raw, EL_STR(""))) {
|
||||
return EL_STR("{\"configured\":false}");
|
||||
}
|
||||
el_val_t _reset = fs_read(EL_STR(""));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"configured\":true,\"contact\":"), raw), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -359,9 +360,8 @@ el_val_t handle_safety_contact_post(el_val_t body) {
|
||||
el_val_t crisis_str = ({ el_val_t _if_result_51 = 0; if (is_crisis) { _if_result_51 = (EL_STR("true")); } else { _if_result_51 = (EL_STR("false")); } _if_result_51; });
|
||||
el_val_t now = time_format(time_now(), EL_STR("%Y-%m-%dT%H:%M:%SZ"));
|
||||
el_val_t contact_json = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"name\":\""), json_safe(name)), EL_STR("\"")), EL_STR(",\"contact_method\":\"")), json_safe(method)), EL_STR("\"")), EL_STR(",\"contact_value\":\"")), json_safe(value)), EL_STR("\"")), EL_STR(",\"relationship\":\"")), json_safe(rel)), EL_STR("\"")), EL_STR(",\"confirmed\":true")), EL_STR(",\"is_crisis_line\":")), crisis_str), EL_STR(",\"set_at\":\"")), now), EL_STR("\"}"));
|
||||
fs_write(safety_contact_path(), contact_json);
|
||||
el_val_t check = fs_read(safety_contact_path());
|
||||
if (str_eq(check, EL_STR(""))) {
|
||||
el_val_t write_ok = fs_write(safety_contact_path(), contact_json);
|
||||
if (write_ok == 0) {
|
||||
return EL_STR("{\"ok\":false,\"error\":\"write_failed\"}");
|
||||
}
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"configured\":true,\"contact\":"), contact_json), EL_STR(",\"ok\":true}"));
|
||||
|
||||
+3
-3
@@ -25754,6 +25754,7 @@ el_val_t handle_safety_contact_get(void) {
|
||||
if (str_eq(raw, EL_STR(""))) {
|
||||
return EL_STR("{\"configured\":false}");
|
||||
}
|
||||
el_val_t _reset = fs_read(EL_STR(""));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"configured\":true,\"contact\":"), raw), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -25773,9 +25774,8 @@ el_val_t handle_safety_contact_post(el_val_t body) {
|
||||
el_val_t crisis_str = ({ el_val_t _if_result_51 = 0; if (is_crisis) { _if_result_51 = (EL_STR("true")); } else { _if_result_51 = (EL_STR("false")); } _if_result_51; });
|
||||
el_val_t now = time_format(time_now(), EL_STR("%Y-%m-%dT%H:%M:%SZ"));
|
||||
el_val_t contact_json = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"name\":\""), json_safe(name)), EL_STR("\"")), EL_STR(",\"contact_method\":\"")), json_safe(method)), EL_STR("\"")), EL_STR(",\"contact_value\":\"")), json_safe(value)), EL_STR("\"")), EL_STR(",\"relationship\":\"")), json_safe(rel)), EL_STR("\"")), EL_STR(",\"confirmed\":true")), EL_STR(",\"is_crisis_line\":")), crisis_str), EL_STR(",\"set_at\":\"")), now), EL_STR("\"}"));
|
||||
fs_write(safety_contact_path(), contact_json);
|
||||
el_val_t check = fs_read(safety_contact_path());
|
||||
if (str_eq(check, EL_STR(""))) {
|
||||
el_val_t write_ok = fs_write(safety_contact_path(), contact_json);
|
||||
if (write_ok == 0) {
|
||||
return EL_STR("{\"ok\":false,\"error\":\"write_failed\"}");
|
||||
}
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"configured\":true,\"contact\":"), contact_json), EL_STR(",\"ok\":true}"));
|
||||
|
||||
@@ -438,6 +438,12 @@ fn safety_contact_path() -> String {
|
||||
fn handle_safety_contact_get() -> String {
|
||||
let raw: String = fs_read(safety_contact_path())
|
||||
if str_eq(raw, "") { return "{\"configured\":false}" }
|
||||
// fs_read set the runtime's binary-safe send length to len(raw); the HTTP
|
||||
// response writer uses that length when non-zero, which would TRUNCATE this
|
||||
// wrapped (longer) response to len(raw). Reset it with a no-op read of a
|
||||
// missing path (fs_read zeroes the length before it opens) so the full
|
||||
// response is sent.
|
||||
let _reset: String = fs_read("")
|
||||
return "{\"configured\":true,\"contact\":" + raw + "}"
|
||||
}
|
||||
|
||||
@@ -463,9 +469,12 @@ fn handle_safety_contact_post(body: String) -> String {
|
||||
+ ",\"confirmed\":true"
|
||||
+ ",\"is_crisis_line\":" + crisis_str
|
||||
+ ",\"set_at\":\"" + now + "\"}"
|
||||
fs_write(safety_contact_path(), contact_json)
|
||||
// Read-back verify the write actually persisted.
|
||||
let check: String = fs_read(safety_contact_path())
|
||||
if str_eq(check, "") { return "{\"ok\":false,\"error\":\"write_failed\"}" }
|
||||
// Verify persistence via fs_write's return (1 = all bytes written, 0 = fail).
|
||||
// The previous fs_read read-back set the runtime's binary-safe send length to
|
||||
// the file size, which then TRUNCATED this longer JSON response to that size
|
||||
// (the safety-contact 988 response was cut mid-"set_at"). Checking the write
|
||||
// return avoids the fs_read entirely, so the full response is sent.
|
||||
let write_ok: Int = fs_write(safety_contact_path(), contact_json)
|
||||
if write_ok == 0 { return "{\"ok\":false,\"error\":\"write_failed\"}" }
|
||||
return "{\"configured\":true,\"contact\":" + contact_json + ",\"ok\":true}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user