Skip to content

tinyweb's own WebSocket, SSE and chunked code breaks on a real network #3109

Closed

Description

@nicolas-maman

tinyweb serves WebSocket and SSE from code of its own: a raw TCP listener on a second and third port (web_server_with_ws, web_server_full) and frame code written over tcp.read and tcp.write. That code passes the tests it has, which send a few short text messages over loopback, but it is not correct on a real network:

WebSocket read (ws_read_frame):

  • Each field is one tcp.read, a single recv(2). A header, a length or a payload that arrives in two TCP segments is read short. For example, a message larger than one segment (about 1.4 KB off loopback) reads only its first part, and the rest is then parsed as the next frame's header.
  • Only the 7-bit and 16-bit lengths are handled. A 64-bit length (marker 127) is taken as a 127-byte payload.
  • tcp.read is the text path, so a binary payload is cut at its first zero byte.
  • A ping is handed to the handler as a text message and never answered with a pong. A fragmented message is not reassembled.
  • A client quiet for 30 s (the socket's receive timeout) reads as a close, so an idle connection is dropped.

WebSocket write (ws_send_frame):

SSE and chunked transfer:

  • SSE events, the SSE headers, the 101 and 404 responses, and chunked bodies are written with one tcp.write each, ignoring the count. sse_send returns nothing, so a handler cannot tell that its client has gone and keeps streaming to a dead socket.
  • write_chunk builds the hex chunk size with string.from_int(digit + 48), which gives the decimal text "53" where it should give the character '5'. Every chunk size it sends is wrong, so no client can read a chunked body from it. Nothing calls it in a test, though the README lists it.

ws_client.ae:

  • contrib/tinyweb/ws_client.ae builds its frame headers with string.from_int(129), which sends the digits "129" where it should send the byte 0x81. Nothing tests it.

Fix: tinyweb's WebSocket and SSE endpoints are served by std.http's server on the main port:

  • WebSocket through http.server_websocket, whose frame code is RFC 6455 conformance-tested against python websockets and handles 64-bit lengths, binary frames, ping/pong, fragments and close codes. The handler's sender is a std.http WebSocket handle.
  • SSE through http.response_upgrade_sse.
  • Chunked streaming through std.http's streaming response.

tinyweb's own listener ports, its frame reader and writer, and ws_client.ae go away; std.http's ws_connect is the client. Tests drive the endpoints with real clients and cover: a message over 64 KiB, a binary payload with zero bytes, a ping, an idle period past 30 s on a shortened timeout, a client that leaves mid-stream (sse_send reports it), and a chunked body read back by std.http's client.

Found on #3101 (#3108).

Configuration that is never applied. with_timeout, with_backlog, with_ws_backlog, with_keep_alive, on_error, on_server_error, on_ws_timeout and on_ws_error store their value in the server's map, and nothing ever reads it back; _invoke_error_handler has no caller. A program that sets a timeout or an error handler gets neither, without a word. The ones std.http's server supports (with_timeout, with_keep_alive: http.server_set_keepalive and its idle timeout) are applied at tw_start; the rest, which nothing underneath can do, are removed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions