{ } qjs-lws

HTTP client

The same LWSContext + protocol mechanism drives outbound HTTP requests. You create a context with no listening port, install an 'http' protocol with the client callbacks, and call ctx.clientConnect(url).

GET with URL string

import { LWSContext, toString } from 'lws';

const ctx = new LWSContext({
  protocols: [{
    name: 'http',
    onEstablishedClientHttp(wsi, status) {
      console.log('status', status, wsi.headers);
    },
    onReceiveClientHttp(wsi) {                    // tells us bytes are ready
      const buf = new ArrayBuffer(64 * 1024);
      if(wsi.httpClientRead(buf))                 // copies them out
        this.onReceiveClientHttpRead(wsi, buf);
    },
    onReceiveClientHttpRead(wsi, data, len) {
      console.log(toString(data, 0, len));
    },
    onCompletedClientHttp(wsi)  { /* body fully received */ },
    onClosedClientHttp(wsi)     { ctx.cancelService(); },
    onClientConnectionError(wsi, msg, errno) {
      console.error('error', msg, errno);
      ctx.cancelService();
    },
  }],
});

ctx.clientConnect('https://blog.fefe.de/');

onReceiveClientHttp is a "data ready" notification — you must call wsi.httpClientRead(buf) yourself to drain the read.

Sending custom request headers

import { WSI_TOKEN_HTTP_ACCEPT, WSI_TOKEN_HTTP_USER_AGENT } from 'lws';

{
  name: 'http',
  onClientAppendHandshakeHeader(wsi, buf, len) {
    wsi.addHeader(WSI_TOKEN_HTTP_ACCEPT,     '*/*',      buf, len);
    wsi.addHeader(WSI_TOKEN_HTTP_USER_AGENT, 'qjs-lws',  buf, len);
    wsi.addHeader('x-custom',                'value',    buf, len);
  },
}

Use the WSI_TOKEN_HTTP_* constants where they exist — they pack more tightly in the request than a literal name.

POST with a body

import { LCCSCF_USE_SSL, LWS_WRITE_HTTP_FINAL } from 'lws';

ctx.clientConnect('https://httpbin.org/post', {
  method: 'POST',
  sslConnection: LCCSCF_USE_SSL,
});

In the protocol:

{
  name: 'http',
  onClientAppendHandshakeHeader(wsi, buf, len) {
    if(!wsi.redirectedToGet && wsi.method === 'POST')
      wsi.bodyPending = 1;                    // tells lws there's a body
  },
  onClientHttpWriteable(wsi) {
    wsi.write('{"hello":"world"}', LWS_WRITE_HTTP_FINAL);
    wsi.bodyPending = 0;
  },
}

wsi.bodyPending is a getter/setter — assigning calls lws_client_http_body_pending().

Multipart upload

import { LCCSCF_HTTP_MULTIPART_MIME } from 'lws';

ctx.clientConnect('https://example.com/upload', {
  method: 'POST',
  sslConnection: LCCSCF_USE_SSL | LCCSCF_HTTP_MULTIPART_MIME,
});
onClientHttpWriteable(wsi) {
  const ab = new ArrayBuffer(4096);
  let len = wsi.clientHttpMultipart('field', null, null, ab);   // text part header
  len += write('value\r\n', ab, len);
  len += wsi.clientHttpMultipart('file', 'a.txt', 'text/plain', ab, len);
  len += write('hello\r\n', ab, len);
  len += wsi.clientHttpMultipart(null, null, null, ab, len);    // closing boundary

  wsi.write(ab, len, LWS_WRITE_HTTP_FINAL);
  wsi.bodyPending = 0;
}

Redirects

onClientHttpRedirect(wsi, url, status) fires before the redirect is followed. Set LCCSCF_HTTP_NO_FOLLOW_REDIRECT in sslConnection to disable redirect following.

wsi.redirectedToGet is true if a POST was downgraded to GET via a 303 redirect.

Connection pipelining / keep-alive

By default every clientConnect() opens its own network connection. Setting LCCSCF_PIPELINE in sslConnection lets lws reuse an existing connection to the same vhost/endpoint instead: for h1, subsequent requests queue on the first ("leader") connection and run sequentially over it; for h2, they join the same network connection as parallel mux streams as soon as it's up. See libwebsockets/lib/core-net/README.md for the full mechanism.

import { LCCSCF_PIPELINE } from 'lws';

const a = ctx.clientConnect(url, { sslConnection: LCCSCF_PIPELINE });
const b = ctx.clientConnect(url, { sslConnection: LCCSCF_PIPELINE });

Both connections still get their own full set of callbacks — lws does not distinguish "leader" from "queued" at the callback level (see lib/core-net/README.md: "The user code does not know which wsi was first or is queued, it just waits for stuff to happen the same either way"). To actually observe whether reuse happened, use the pipelineLeader / isPipelineLeader / pipelineQueueDepth accessors on LWSSocket — see LWSSocket.md.

These accessors require the lws_get_txn_queue_leader() / lws_wsi_is_txn_queue_leader() / lws_get_txn_queue_depth() patch to the vendored libwebsockets in patches/; upstream lws does not expose this state.

Set LCCSCF_PIPELINE in ssl_connection to reuse one shared LWSContext/vhost across calls so repeat requests to the same origin can queue/mux onto an existing connection. Pass keepAlive: false, or a custom tls option, to get an isolated one-off context instead.