• JS http.js: 2nd HTTPS request to same host fails with 'Unable to read

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Mon May 25 18:36:22 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1148

    ## Summary

    Any second HTTPS GET to the **same host** in a single Synchronet JS process fails with `Error: Unable to read status` thrown out of `HTTPRequest.ReadStatus()`. The first HTTPS request to a given host succeeds; the second fails. Plain HTTP repeats (no TLS) work fine. Looks like a TLS-session-cache issue in the cryptlib-backed socket layer, not in `exec/load/http.js` itself.

    ## Minimal Repro

    Run in `jsexec` (Win32 release build, but seen across builds):

    ```javascript
    require('http.js', 'HTTPRequest');
    for (var i = 1; i <= 3; i++) {
    var h = new HTTPRequest(undefined, undefined, undefined, 30);
    try {
    h.Get("https://gitlab.synchro.net/api/v4/projects/13");
    writeln("call " + i + ": rc=" + h.response_code + " body.len=" + h.body.length);
    } catch (e) {
    writeln("call " + i + ": ERR: " + e);
    }
    }
    ```

    Output:

    ```
    call 1: rc=200 body.len=946
    call 2: ERR: Error: Unable to read status
    call 3: ERR: Error: Unable to read status
    ```

    The error originates at `HTTPRequest.prototype.ReadStatus` in `exec/load/http.js`:

    ```javascript
    HTTPRequest.prototype.ReadStatus = function() {
    this.status_line = this.sock.recvline(4096, this.recv_timeout);
    if (this.status_line == null)
    throw new Error("Unable to read status");
    // ...
    };
    ```

    `this.sock.recvline()` returns null on the 2nd request to the same host, indicating the TLS handshake either fails or the connection drops before any status bytes arrive.

    ## Things I Tried That Did NOT Fix It

    - Using a fresh `HTTPRequest` object per call (not just reusing one)
    - Explicitly `http.sock.close()` and `http.sock = undefined` between calls
    - Sleeping 2s between calls (`mswait(2000)`)
    - Clearing `http.body = undefined` before the 2nd `Get` (in case `SetupGet` was reusing it -- it does NOT reset `this.body`, which is a latent bug but not the cause here)
    - Letting the redirect-follow logic run (no redirects involved at this URL)

    ## Things That DO Work

    - A second HTTPS request to a **DIFFERENT host** succeeds (call 1 to gitlab → call 2 to httpbin.org → both work)
    - BUT after the intervening different-host call, going BACK to the original host on call 3 fails again
    - Plain HTTP repeats to the same host: all calls succeed (no TLS, no problem)

    ## Hypothesis

    The cryptlib `ssl_session` socket attribute (set by `http.js` `SendRequest` at line 129) caches per-host TLS session state. On a 2nd connection to the same host the cached session is stale / invalid for resumption, and the new TCP+TLS attempt either:

    a) Tries to resume a closed session and the handshake fails silently
    b) Cryptlib's per-host session cache holds a reference to the freed previous-socket session and corrupts the new connection

    Either way `recvline()` returns null instead of a status line.

    ## Impact

    Blocks any JS module that wants to make multiple HTTPS requests in one process, such as:

    - `chat_llm.js` retrieval crawlers wanting to use the GitLab REST API (`/api/v4/projects/<id>/issues` is paginated -- a real-world script needs 10+ calls to fetch all issues)
    - Any future module pulling RSS / web content for grounding / mirroring
    - IRC adapters that want to do periodic HTTPS polls

    Current workaround in `chat_llm.js` is to read GitLab data from locally captured webhook JSONL files (`data/gitissue.jsonl`, `data/gitpush.jsonl`) instead of the API. Works on vert (which receives the upstream webhook feed) but isn't portable to other Synchronet installs.

    ## Environment

    - Synchronet 3.22a (Win32, Release build)
    - Single-process jsexec invocation
    - Affected hosts: gitlab.synchro.net, httpbin.org (any HTTPS host)
    - Cryptlib version: whatever ships in this build

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Mon May 25 20:39:44 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1148#note_9064

    When I run it on my system:

    ```
    Reading script from /tmp/issue1148/repro.js
    /tmp/issue1148/repro.js compiled in 0.00 seconds
    call 1: rc=200 body.len=946
    call 2: rc=200 body.len=946
    call 3: rc=200 body.len=946
    /tmp/issue1148/repro.js executed in 2.73 seconds
    ```
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab issue in main/sbbs on Mon May 25 20:43:36 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1148
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Mon May 25 20:43:37 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1148#note_9065

    Not reproducible, and the diagnosis is wrong. Closing as invalid.

    ## Doesn't reproduce

    I ran the supplied repro verbatim against a live install and got three successful HTTPS calls — `rc=200` on call 1, 2, and 3. No
    "Unable to read status" error.

    ## The hypothesis is incorrect

    The report claims `sock.ssl_session = true` caches a per-host TLS session
    that becomes stale on the second connection. That's not what
    `ssl_session` does. Setting it triggers `cryptCreateSession()` for a
    brand-new `CRYPT_SESSION_TLS` on this specific socket — see `src/sbbs3/js_socket.cpp:2348` (`SOCK_PROP_SSL_SESSION` handler). There
    is no per-host TLS session cache in this code path.

    Each `HTTPRequest.Get()` call also opens a fresh TCP connection (`exec/load/http.js:113-127` — closes any previous `this.sock`, then
    calls `new Socket()` and `connect()`). The default headers include
    `Connection: close` (`http.js:35`), so no keepalive or session
    resumption is attempted on the wire either.

    ## The bug, if real, would break ACME

    `exec/load/acmev2.js` is the built-in Let's Encrypt client. It reuses a
    single `HTTPRequest` to issue many HTTPS calls to `acme-v02.api.letsencrypt.org` during cert issuance/renewal. If the
    second HTTPS call to a given host always failed, every sysop relying on
    the bundled ACME client would have a broken renewal — which isn't what
    we see in the field. `exec/init-fidonet.js` similarly pulls multiple
    HTTPS URLs in a loop without issue.

    ## Unverifiable artifacts

    The "Impact" section cites `chat_llm.js`, `data/gitissue.jsonl`, and `data/gitpush.jsonl`. None of these exist in the Synchronet tree or are produced by any Synchronet component, so the stated impact and
    workaround can't be evaluated.

    If a real second-HTTPS-call failure does show up against a specific
    host, please reopen with: the actual URL, the cryptlib log lines from
    that run (`data/error.log` and the per-day terminal-server log under `data/logs/`), and a single end-to-end transcript of jsexec running the
    repro — not a hypothesis.
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Mon May 25 21:20:56 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1148#note_9066

    Attaching the repro script as a standalone file for convenience: [repro1148.js](/uploads/504a5aa05699c38f3fa610f6f7ab7454/repro1148.js)

    It's the same code as in the issue description, just easier to `wget`/`curl` and run as-is. Reproduces here on Windows against current master, in both Release (`master/edf752429`) and Debug (`master/0db1c34fc`) builds — both link the same prebuilt `cl32.dll` from `3rdp/win32.release/cryptlib/bin/` via symlink, so it's not a cryptlib-version split. Stashing the local working-tree changes to `exec/load/http.js` (unrelated streaming-POST additions to `Post()`'s prototype) also doesn't change the result — the `Get()` code path is unmodified.

    Going to test on Linux next to see if it's Windows-specific.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Mon May 25 21:25:50 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1148#note_9067

    Confirmed non-reproduction on Linux against current master.

    Ran the repro script verbatim from `/sbbs/jsexec`:

    ```
    $ jsexec /tmp/issue1148/repro1148.js
    call 1: rc=200 body.len=946
    call 2: rc=200 body.len=946
    call 3: rc=200 body.len=946
    ```

    Also extended to 5 iterations × 3 hosts (gitlab.synchro.net, httpbin.org, www.google.com) — all 15 HTTPS GETs returned rc=200 with sane body sizes, no `Unable to read status` thrown. Matches Deuce's earlier non-repro.

    So the failure mode appears Windows-specific. Leaving the issue closed; if it resurfaces on Windows I'll reopen with a cryptlib log capture (`data/error.log` + per-day TS log) from a failing run, plus a test against a different `cl32.dll` build to rule the bundled cryptlib in or out.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Mon May 25 21:45:09 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1148#note_9068

    Two corrections to my prior follow-up:

    **Log-file references were wrong.** The closing line suggested capturing `data/error.log` + the per-day Terminal Server log if it resurfaces on Windows. Neither captures a `jsexec` failure — `data/error.log` is the server-side `errormsg()`/`LOG_ERR` sink, and `data/logs/<date>.log` records Terminal Server *sessions*. `jsexec` is standalone and writes only to its own stdout/stderr (or `-L <file>` / `-e <file>`). The actually useful diagnostic here would be the cryptlib status code/string from the failing socket op, surfaced through `js_socket.cpp` — which currently propagates only as a bare `"Unable to read status"` JS exception with no underlying error context. A small `lprintf` or exception-message enrichment that includes the cryptlib reason would make this kind of bug far easier to triage next time.

    **"If it resurfaces on Windows" understates the state.** It hasn't gone away. It reproduces 100% on Windows against current master in both Release (`master/edf752429`) and Debug (`master/0db1c34fc`) builds with the verbatim repro from this issue. The Linux non-reproduction localizes the bug; it doesn't fix it. Leaving the issue closed since the original report misdiagnosed the cause, but the underlying Windows failure is reliably present, not a "watch for it" item.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)