beam.com 0.1.0-rc.4 → 0.1.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -219,7 +219,7 @@ jobs:
219
219
 
220
220
  Then turn on Pages one time: Settings, Pages, Source "GitHub Actions". The
221
221
  `GITHUB_TOKEN` cannot do this step. Until the release `v0.1.0`, use the
222
- release candidate `@v0.1.0-rc.4` in place of `@v0.1.0`. With `@main`, the
222
+ release candidate `@v0.1.0-rc.5` in place of `@v0.1.0`. With `@main`, the
223
223
  workflow uses the `edge` build of `beam.com`.
224
224
 
225
225
  The workflow ([`pages-app.yml`](.github/workflows/pages-app.yml)) builds
@@ -286,7 +286,7 @@ Each GitHub release from 0.1.0-rc.2 also holds the package, the same
286
286
  file as on npm. npm installs it with no registry:
287
287
 
288
288
  ```sh
289
- npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.4/beam.com-0.1.0-rc.4.tgz
289
+ npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.5/beam.com-0.1.0-rc.5.tgz
290
290
  ```
291
291
 
292
292
  - `BEAM_COM`: the path of a `beam.com` to use. Then nothing is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beam.com",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.5",
4
4
  "description": "Erlang/OTP and Elixir in one executable file: npx beam.com, and the WebAssembly runtime for a native app.com",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
36
36
  import { Vm } from './worker.js';
37
37
 
38
38
  const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
39
+
40
+ // The request to the object stub. workerd pipes the body from the client
41
+ // to the object, and that pipe reads on after the response has gone when
42
+ // the object stops its read early. The read then fails with no handler:
43
+ // "Uncaught TypeError: Can't read from request stream after response has
44
+ // been sent". Here the pipe is one of this Worker, with a handler. A
45
+ // FixedLengthStream (workerd) keeps the length of the body.
46
+ export function forward(stub, request) {
47
+ if (!request.body) return stub.fetch(request);
48
+ const length = Number(request.headers.get('content-length') ?? NaN);
49
+ const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
50
+ ? new FixedLengthStream(length) : new TransformStream();
51
+ request.body.pipeTo(pass.writable).catch(() => {});
52
+ return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
53
+ }
39
54
  const REGISTRY = '.registry';
40
55
  const EXPIRES = 'beam:expires';
41
56
 
@@ -363,7 +378,7 @@ export default {
363
378
  // The app can trust the header: a client cannot give it.
364
379
  request = new Request(inner, request);
365
380
  request.headers.set('x-beam-tenant', m[1]);
366
- const response = await env.BEAM.get(env.BEAM.idFromName(m[1])).fetch(request);
381
+ const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
367
382
  if (!response.headers.has('service-worker-allowed')) return response;
368
383
  const out = new Response(response.body, response);
369
384
  out.headers.delete('service-worker-allowed');
@@ -386,6 +401,6 @@ export default {
386
401
  request = new Request(request);
387
402
  if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
388
403
  else request.headers.delete('x-beam-tenant');
389
- return env.BEAM.get(env.BEAM.idFromName(name)).fetch(request);
404
+ return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
390
405
  },
391
406
  };
@@ -28,7 +28,7 @@ import { env as globalEnv } from 'cloudflare:workers';
28
28
  import { release, use } from './release.js';
29
29
  import { useSnapshot } from './snapshot.js';
30
30
  import plain, { parseSnapshot, staticResponse, Vm } from './worker.js';
31
- import front from './durable.js';
31
+ import front, { forward } from './durable.js';
32
32
 
33
33
  export { Beam } from './durable.js';
34
34
 
@@ -113,7 +113,7 @@ export function serve(app, { binding = 'BEAM', name, nifs = null, snapshot = nul
113
113
  const objects = env[binding];
114
114
  if (!objects && vm) return vm.fetch(request, ctx);
115
115
  if (!objects) return plain.fetch(request, env, ctx);
116
- if (typeof name === 'function') return objects.getByName(name(request)).fetch(request);
116
+ if (typeof name === 'function') return forward(objects.getByName(name(request)), request);
117
117
  return front.fetch(request, frontEnv(env), ctx);
118
118
  },
119
119
  scheduled(controller, env, ctx) {
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,7 +1986,7 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
@@ -1974,14 +1994,20 @@ export class Vm {
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
1995
  // With a body, the head goes in the event of the first part of the
1976
1996
  // body (bridgeUpload): a small body is one event, as before the parts.
1977
- if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1978
1998
  else this.bridgeSend(c, bytes);
1979
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1980
2005
  }
1981
2006
 
1982
2007
  // Bytes to the app on the connection c.
1983
2008
  bridgeSend(c, bytes) {
1984
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1985
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1986
2012
  }
1987
2013
 
@@ -1989,9 +2015,12 @@ export class Vm {
1989
2015
  // (or the rest of the body): one event for each part, not for each small
1990
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1991
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1992
- // the memory of the VM, and a slow app slows the client. The head of the
1993
- // request goes with the bytes of the first read of the client, with no
1994
- // wait for UPLOAD_PART bytes.
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
1995
2024
  async bridgeUpload(c, body, chunked, max, head) {
1996
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1997
2026
  // not safe: when the stream closes with fewer bytes than min, the read
@@ -2046,7 +2075,7 @@ export class Vm {
2046
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2047
2076
  part.push(piece);
2048
2077
  size += piece.length;
2049
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2050
2079
  }
2051
2080
  if (ok && (done || head) && size) ok = await flush();
2052
2081
  if (!ok) break;
@@ -2057,11 +2086,15 @@ export class Vm {
2057
2086
  return;
2058
2087
  }
2059
2088
  }
2060
- reader.cancel().catch(() => {});
2061
2089
  } catch {
2062
2090
  // The client stopped the upload.
2063
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2064
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2065
2098
  }
2066
2099
 
2067
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2069,9 +2102,12 @@ export class Vm {
2069
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2070
2103
  }
2071
2104
 
2072
- // The app read n bytes of the connection c (tcp_read).
2073
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2074
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2075
2111
  this.bridgeRoom(c);
2076
2112
  c.inbound?.flush();
2077
2113
  }
@@ -2681,7 +2717,7 @@ export class Vm {
2681
2717
  }
2682
2718
  // wasm_tcp: the app read n bytes of a socket.
2683
2719
  case 'tcp_read': {
2684
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2685
2721
  break;
2686
2722
  }
2687
2723
  case 'tcp_close': {
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
2774
2810
  // costs a turn of the VM.
2775
2811
  const UPLOAD_WINDOW = 256 * 1024;
2776
2812
  const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2777
2814
  // The time that the head of a request waits for the first bytes of its
2778
2815
  // body (bridgeUpload), in ms.
2779
2816
  const HEAD_WAIT = 20;
2780
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2781
2856
 
2782
2857
  // The size bytes of the list parts, as one array.
2783
2858
  function join(parts, size) {
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,7 +1986,7 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
@@ -1974,14 +1994,20 @@ export class Vm {
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
1995
  // With a body, the head goes in the event of the first part of the
1976
1996
  // body (bridgeUpload): a small body is one event, as before the parts.
1977
- if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1978
1998
  else this.bridgeSend(c, bytes);
1979
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1980
2005
  }
1981
2006
 
1982
2007
  // Bytes to the app on the connection c.
1983
2008
  bridgeSend(c, bytes) {
1984
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1985
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1986
2012
  }
1987
2013
 
@@ -1989,9 +2015,12 @@ export class Vm {
1989
2015
  // (or the rest of the body): one event for each part, not for each small
1990
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1991
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1992
- // the memory of the VM, and a slow app slows the client. The head of the
1993
- // request goes with the bytes of the first read of the client, with no
1994
- // wait for UPLOAD_PART bytes.
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
1995
2024
  async bridgeUpload(c, body, chunked, max, head) {
1996
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1997
2026
  // not safe: when the stream closes with fewer bytes than min, the read
@@ -2046,7 +2075,7 @@ export class Vm {
2046
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2047
2076
  part.push(piece);
2048
2077
  size += piece.length;
2049
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2050
2079
  }
2051
2080
  if (ok && (done || head) && size) ok = await flush();
2052
2081
  if (!ok) break;
@@ -2057,11 +2086,15 @@ export class Vm {
2057
2086
  return;
2058
2087
  }
2059
2088
  }
2060
- reader.cancel().catch(() => {});
2061
2089
  } catch {
2062
2090
  // The client stopped the upload.
2063
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2064
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2065
2098
  }
2066
2099
 
2067
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2069,9 +2102,12 @@ export class Vm {
2069
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2070
2103
  }
2071
2104
 
2072
- // The app read n bytes of the connection c (tcp_read).
2073
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2074
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2075
2111
  this.bridgeRoom(c);
2076
2112
  c.inbound?.flush();
2077
2113
  }
@@ -2681,7 +2717,7 @@ export class Vm {
2681
2717
  }
2682
2718
  // wasm_tcp: the app read n bytes of a socket.
2683
2719
  case 'tcp_read': {
2684
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2685
2721
  break;
2686
2722
  }
2687
2723
  case 'tcp_close': {
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
2774
2810
  // costs a turn of the VM.
2775
2811
  const UPLOAD_WINDOW = 256 * 1024;
2776
2812
  const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2777
2814
  // The time that the head of a request waits for the first bytes of its
2778
2815
  // body (bridgeUpload), in ms.
2779
2816
  const HEAD_WAIT = 20;
2780
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2781
2856
 
2782
2857
  // The size bytes of the list parts, as one array.
2783
2858
  function join(parts, size) {
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
36
36
  import { Vm } from './worker.js';
37
37
 
38
38
  const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
39
+
40
+ // The request to the object stub. workerd pipes the body from the client
41
+ // to the object, and that pipe reads on after the response has gone when
42
+ // the object stops its read early. The read then fails with no handler:
43
+ // "Uncaught TypeError: Can't read from request stream after response has
44
+ // been sent". Here the pipe is one of this Worker, with a handler. A
45
+ // FixedLengthStream (workerd) keeps the length of the body.
46
+ export function forward(stub, request) {
47
+ if (!request.body) return stub.fetch(request);
48
+ const length = Number(request.headers.get('content-length') ?? NaN);
49
+ const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
50
+ ? new FixedLengthStream(length) : new TransformStream();
51
+ request.body.pipeTo(pass.writable).catch(() => {});
52
+ return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
53
+ }
39
54
  const REGISTRY = '.registry';
40
55
  const EXPIRES = 'beam:expires';
41
56
 
@@ -363,7 +378,7 @@ export default {
363
378
  // The app can trust the header: a client cannot give it.
364
379
  request = new Request(inner, request);
365
380
  request.headers.set('x-beam-tenant', m[1]);
366
- const response = await env.BEAM.get(env.BEAM.idFromName(m[1])).fetch(request);
381
+ const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
367
382
  if (!response.headers.has('service-worker-allowed')) return response;
368
383
  const out = new Response(response.body, response);
369
384
  out.headers.delete('service-worker-allowed');
@@ -386,6 +401,6 @@ export default {
386
401
  request = new Request(request);
387
402
  if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
388
403
  else request.headers.delete('x-beam-tenant');
389
- return env.BEAM.get(env.BEAM.idFromName(name)).fetch(request);
404
+ return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
390
405
  },
391
406
  };
@@ -1,3 +1,3 @@
1
1
  // The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
2
2
  // the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
3
- export default '3ef0d2bdd150e2ab64517e29e6e85fbf70382d878b9ccbca998a22f15b566dc0';
3
+ export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,7 +1986,7 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
@@ -1974,14 +1994,20 @@ export class Vm {
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
1995
  // With a body, the head goes in the event of the first part of the
1976
1996
  // body (bridgeUpload): a small body is one event, as before the parts.
1977
- if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1978
1998
  else this.bridgeSend(c, bytes);
1979
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1980
2005
  }
1981
2006
 
1982
2007
  // Bytes to the app on the connection c.
1983
2008
  bridgeSend(c, bytes) {
1984
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1985
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1986
2012
  }
1987
2013
 
@@ -1989,9 +2015,12 @@ export class Vm {
1989
2015
  // (or the rest of the body): one event for each part, not for each small
1990
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1991
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1992
- // the memory of the VM, and a slow app slows the client. The head of the
1993
- // request goes with the bytes of the first read of the client, with no
1994
- // wait for UPLOAD_PART bytes.
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
1995
2024
  async bridgeUpload(c, body, chunked, max, head) {
1996
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1997
2026
  // not safe: when the stream closes with fewer bytes than min, the read
@@ -2046,7 +2075,7 @@ export class Vm {
2046
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2047
2076
  part.push(piece);
2048
2077
  size += piece.length;
2049
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2050
2079
  }
2051
2080
  if (ok && (done || head) && size) ok = await flush();
2052
2081
  if (!ok) break;
@@ -2057,11 +2086,15 @@ export class Vm {
2057
2086
  return;
2058
2087
  }
2059
2088
  }
2060
- reader.cancel().catch(() => {});
2061
2089
  } catch {
2062
2090
  // The client stopped the upload.
2063
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2064
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2065
2098
  }
2066
2099
 
2067
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2069,9 +2102,12 @@ export class Vm {
2069
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2070
2103
  }
2071
2104
 
2072
- // The app read n bytes of the connection c (tcp_read).
2073
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2074
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2075
2111
  this.bridgeRoom(c);
2076
2112
  c.inbound?.flush();
2077
2113
  }
@@ -2681,7 +2717,7 @@ export class Vm {
2681
2717
  }
2682
2718
  // wasm_tcp: the app read n bytes of a socket.
2683
2719
  case 'tcp_read': {
2684
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2685
2721
  break;
2686
2722
  }
2687
2723
  case 'tcp_close': {
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
2774
2810
  // costs a turn of the VM.
2775
2811
  const UPLOAD_WINDOW = 256 * 1024;
2776
2812
  const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2777
2814
  // The time that the head of a request waits for the first bytes of its
2778
2815
  // body (bridgeUpload), in ms.
2779
2816
  const HEAD_WAIT = 20;
2780
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2781
2856
 
2782
2857
  // The size bytes of the list parts, as one array.
2783
2858
  function join(parts, size) {
@@ -1 +1 @@
1
- {"version": "0.1.0-rc.4", "sha256": "686c7f660def2d824d0580717d02736b2add06db7f06becbd3d6ff66cd5d2711"}
1
+ {"version": "0.1.0-rc.5", "sha256": "ac36bab20b0cf8a6bcd9b9e02bae1e9d356855a75f4664ac65c58ff6fed8c024"}
@@ -1,3 +1,3 @@
1
1
  // The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
2
2
  // the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
3
- export default '3ef0d2bdd150e2ab64517e29e6e85fbf70382d878b9ccbca998a22f15b566dc0';
3
+ export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
package/runtime/worker.js CHANGED
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,7 +1986,7 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
@@ -1974,14 +1994,20 @@ export class Vm {
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
1995
  // With a body, the head goes in the event of the first part of the
1976
1996
  // body (bridgeUpload): a small body is one event, as before the parts.
1977
- if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1978
1998
  else this.bridgeSend(c, bytes);
1979
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1980
2005
  }
1981
2006
 
1982
2007
  // Bytes to the app on the connection c.
1983
2008
  bridgeSend(c, bytes) {
1984
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1985
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1986
2012
  }
1987
2013
 
@@ -1989,9 +2015,12 @@ export class Vm {
1989
2015
  // (or the rest of the body): one event for each part, not for each small
1990
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1991
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1992
- // the memory of the VM, and a slow app slows the client. The head of the
1993
- // request goes with the bytes of the first read of the client, with no
1994
- // wait for UPLOAD_PART bytes.
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
1995
2024
  async bridgeUpload(c, body, chunked, max, head) {
1996
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1997
2026
  // not safe: when the stream closes with fewer bytes than min, the read
@@ -2046,7 +2075,7 @@ export class Vm {
2046
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2047
2076
  part.push(piece);
2048
2077
  size += piece.length;
2049
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2050
2079
  }
2051
2080
  if (ok && (done || head) && size) ok = await flush();
2052
2081
  if (!ok) break;
@@ -2057,11 +2086,15 @@ export class Vm {
2057
2086
  return;
2058
2087
  }
2059
2088
  }
2060
- reader.cancel().catch(() => {});
2061
2089
  } catch {
2062
2090
  // The client stopped the upload.
2063
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2064
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2065
2098
  }
2066
2099
 
2067
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2069,9 +2102,12 @@ export class Vm {
2069
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2070
2103
  }
2071
2104
 
2072
- // The app read n bytes of the connection c (tcp_read).
2073
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2074
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2075
2111
  this.bridgeRoom(c);
2076
2112
  c.inbound?.flush();
2077
2113
  }
@@ -2681,7 +2717,7 @@ export class Vm {
2681
2717
  }
2682
2718
  // wasm_tcp: the app read n bytes of a socket.
2683
2719
  case 'tcp_read': {
2684
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2685
2721
  break;
2686
2722
  }
2687
2723
  case 'tcp_close': {
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
2774
2810
  // costs a turn of the VM.
2775
2811
  const UPLOAD_WINDOW = 256 * 1024;
2776
2812
  const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2777
2814
  // The time that the head of a request waits for the first bytes of its
2778
2815
  // body (bridgeUpload), in ms.
2779
2816
  const HEAD_WAIT = 20;
2780
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2781
2856
 
2782
2857
  // The size bytes of the list parts, as one array.
2783
2858
  function join(parts, size) {