Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha

## [Unreleased]

### Fixed
- Gateway static responses continue across HTTP writable callbacks. The dashboard and hardware scripts gzip past the 4096-byte chunk, and the first chunk was the last one sent, so the browser waited on `Content-Length` and the UI never started.

## [1.0.1] - 2026-10-18

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions src/gateway/http_lws.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ int http_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user,
free(conn);
lws_set_wsi_user(wsi, NULL);
http_lws_tx_completed(wsi);
} else {
/* At most 4096 body bytes leave per callback. Dashboard
* and hardware JS gzip past that, so without another
* writable request the response stalls at Content-Length. */
lws_callback_on_writable(wsi);
}
return 0;
}
Expand Down
89 changes: 89 additions & 0 deletions tests/test_gateway_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,91 @@ static int http_get(const char *url, long *code_out, char **body_out)
return (res == CURLE_OK) ? 0 : -1;
}

struct http_raw_body {
unsigned char data[65536];
size_t len;
};

static size_t write_raw_cb(char *ptr, size_t size, size_t nmemb, void *user)
{
struct http_raw_body *body = user;
size_t total = size * nmemb;

if (!body || body->len + total > sizeof(body->data))
return 0;
memcpy(body->data + body->len, ptr, total);
body->len += total;
return total;
}

static int http_get_raw(const char *url, long *code_out, struct http_raw_body *body,
curl_off_t *cl_out)
{
CURL *curl;
CURLcode res;
long code = 0;
curl_off_t cl = -1;

if (!body)
return -1;
curl = curl_easy_init();
if (!curl)
return -1;
body->len = 0;
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_raw_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_HTTP_CONTENT_DECODING, 0L);
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl);
curl_easy_cleanup(curl);
if (code_out)
*code_out = code;
if (cl_out)
*cl_out = cl;
return (res == CURLE_OK) ? 0 : -1;
}

static int raw_body_is_full_gzip(const struct http_raw_body *body, long code,
curl_off_t cl)
{
if (!body || code != 200 || cl <= 4096)
return 0;
if (body->len != (size_t)cl || body->len < 2)
return 0;
return body->data[0] == 0x1f && body->data[1] == 0x8b;
}

/* Static HTTP sends at most 4096 bytes per writable callback. These scripts
* gzip larger than that, so a stalled callback returns a short body. */
static int test_static_js_body_completes(void)
{
static const char *paths[] = { "/js/app.js", "/js/hardware.js" };
struct http_raw_body *body;
size_t i;

body = calloc(1, sizeof(*body));
if (!body)
return 1;
for (i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
long code = 0;
curl_off_t cl = -1;
int rc = http_get_raw(gw_url(paths[i]), &code, body, &cl);

if (rc != 0 || !raw_body_is_full_gzip(body, code, cl)) {
fprintf(stderr,
"static %s incomplete rc=%d code=%ld cl=%lld len=%zu\n",
paths[i], rc, code, (long long)cl, body->len);
free(body);
return 1;
}
}
free(body);
return 0;
}

static int http_post(const char *url, const char *json, long *code_out, char **body_out)
{
return http_post_raw(url, json, json ? strlen(json) : 0, NULL, code_out, body_out);
Expand Down Expand Up @@ -1713,6 +1798,10 @@ int main(int argc, char **argv)
failed++;
}
if (test_health() != 0) { fprintf(stderr, "test_health failed\n"); failed++; }
if (test_static_js_body_completes() != 0) {
fprintf(stderr, "test_static_js_body_completes failed\n");
failed++;
}
if (test_pair(pairing_code, token, sizeof(token)) != 0) {
fprintf(stderr, "test_pair failed\n");
failed++;
Expand Down
Loading