Skip to content

feat(websockets): add manual WebSockets.upgrade(f, stream) for mixed HTTP/WS servers#1255

Merged
quinnj merged 3 commits into
masterfrom
jq-ws-upgrade
May 29, 2026
Merged

feat(websockets): add manual WebSockets.upgrade(f, stream) for mixed HTTP/WS servers#1255
quinnj merged 3 commits into
masterfrom
jq-ws-upgrade

Conversation

@quinnj
Copy link
Copy Markdown
Member

@quinnj quinnj commented May 29, 2026

Summary

HTTP.jl 1.x let you upgrade an in-flight server stream to a WebSocket by hand
(WebSockets.upgrade(f, http::Stream)), which is how a single HTTP.listen! /
HTTP.Router server could serve both ordinary HTTP routes and WebSocket routes.
The 2.0 rewrite only shipped the standalone WebSockets.listen! accept loop, so
this manual path went missing. This PR restores it.

HTTP.listen!("127.0.0.1", 8080) do stream
    if HTTP.WebSockets.isupgrade(stream.message)
        HTTP.WebSockets.upgrade(stream) do ws
            for msg in ws
                HTTP.WebSockets.send(ws, msg)
            end
        end
    else
        HTTP.setstatus(stream, 200)
        HTTP.startwrite(stream)
        write(stream, "ok")
    end
end

How it works

  • Writes the 101 handshake straight to the connection, then takes the socket
    away from the HTTP/1 server loop using the existing write_closed /
    read_closed flags plus response.close, so the loop's post-handler
    teardown becomes a no-op and it stops reading further requests off what is now
    a WebSocket.
  • Clears the per-request read deadline the server armed before invoking the
    handler, so a long-lived WebSocket isn't torn down mid-session (covered by a
    test that idles longer than read_timeout).
  • Hands any bytes buffered past the handshake request to the WebSocket codec
    (mirrors the existing client handshake path).
  • Rejects upgrades over HTTP/2 with a clear error.

Changes

  • WebSockets.upgrade(f, stream) — new public API.
  • _upgrade_response parameterized by subprotocols / check_origin; the
    Server method is now a thin wrapper over it (no behavior change — existing
    server origin / subprotocol / invalid-key tests still pass).
  • Server Streams now retain their per-connection reader so upgrade can
    recover buffered bytes. Server body reads go through request_body, so
    stream.reader is otherwise unused on the server side.
  • Migration guide section documenting upgrade.

Tests

New integration tests: mixed HTTP + WS on one listen! (text + binary echo),
the read-deadline-clearing case, and rejection of non-upgradeable streams.
Existing WebSocket client / server / integration suites and the HTTP/1 server
suite still pass locally.

🤖 Generated with Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 68.08511% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.36%. Comparing base (47fd097) to head (dc79fe1).

Files with missing lines Patch % Lines
src/http_websockets.jl 67.39% 15 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1255      +/-   ##
==========================================
- Coverage   84.50%   84.36%   -0.14%     
==========================================
  Files          28       28              
  Lines       10648    10689      +41     
==========================================
+ Hits         8998     9018      +20     
- Misses       1650     1671      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

quinnj and others added 2 commits May 29, 2026 10:23
Restore the 1.x ability to upgrade an in-flight HTTP/1.1 server stream to a
WebSocket from inside a normal HTTP.listen!/Router stream handler, so a single
server can mix ordinary HTTP routes and WebSocket routes. The 2.0 rewrite only
shipped the standalone WebSockets.listen! accept loop, dropping this path.

- WebSockets.upgrade(f, stream) writes the 101 handshake directly to the
  connection, then hijacks it from the HTTP/1 server loop via the existing
  write_closed/read_closed flags + response.close, clears the per-request read
  deadline so long-lived sockets are not torn down mid-session, and hands any
  bytes buffered past the handshake to the WebSocket codec.
- Refactor _upgrade_response to take subprotocols/check_origin directly so it no
  longer requires a WebSockets.Server; the Server method is kept as a thin
  wrapper (no behavior change).
- Server Streams now retain their per-connection reader; server body reads go
  through request_body, so stream.reader is otherwise unused on the server.
- Reject upgrades over HTTP/2 with a clear error.
- Document upgrade in the 1.x migration guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a docstring to isupgrade and list HTTP.WebSockets.upgrade /
HTTP.WebSockets.isupgrade in the WebSockets API reference so the new manual
upgrade entrypoint and its guard are rendered and their @ref links resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Land the WebSocket upgrade feature together with the intermittent-Windows-hang
fix it was blocked on:
- runtests.jl hang watchdog that dumps task backtraces on suite timeout (how the
  root cause was captured)
- bound _read_until_quiet with the _run_with_timeout watchdog so a Reseau
  read-deadline strand (JuliaServices/Reseau.jl#107) fails fast instead of
  hanging; covers direct callers, not just _raw_http_request
- guard the write-timeout test's take! so a missed timeout cannot block on an
  empty channel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@quinnj quinnj merged commit 63bfadb into master May 29, 2026
6 of 8 checks passed
@quinnj quinnj deleted the jq-ws-upgrade branch May 29, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant