Repository navigation
Support both HTTP.jl 1.x and 2.x - #3608
Merged
Merged
Conversation
HTTP.jl 2 is a breaking release: it moves the transport layer to Reseau, turns `Headers` and message bodies into their own types, checks the `Origin` header on WebSocket upgrades, and changed the shape of `listen!`. Rather than migrating away from 1.x (#3565), widen compat to `^1.10.17, 2.4` and adapt. 2.4 is the minimum on the 2.x line because 2.0–2.3 carry the HTTP/2, WebSocket masking and request handling advisories fixed in 2.4.0. The version-dependent pieces live in the new `src/webserver/HTTPCompat.jl`, behind a `HTTP_IS_V2` compile-time constant: the listening socket type, the WebSocket `Origin` policy, the in-memory request body type, who writes `Content-Length`, and the exception a client disconnect surfaces as. Everything else is written once. `run!` now hands the request/response path to `HTTP.streamhandler`, which both versions provide and which does exactly what Pluto was doing by hand; the headers Pluto adds to every response and the Binder `token` capture moved into middleware so they apply on both. The `on_shutdown` hook is called by `Base.close(::RunningPlutoServer)` instead of by HTTP.jl, since 2.x has no such hook and both versions close the server gracefully — i.e. they wait for the open WebSockets that `on_shutdown` closes. Origin checking is disabled on the upgrade (HTTP.jl 1 does not check at all). The default policy compares the `Origin` against the request's own scheme, host and port, which rejects legitimate clients behind the TLS-terminating, `Host`-rewriting proxies Pluto is regularly served through (Binder, JupyterHub, JuliaHub). Pluto authenticates an upgrade with its session secret, whose cookie is `SameSite=Strict` and so is not sent cross-site. Tests: a new `test/webserver.jl` testset covers response framing and headers, reading a request body, a WebSocket round trip with a cross-site `Origin`, and a graceful shutdown; `Test.yml` gains four jobs that pin HTTP.jl 1.x, and the testset asserts the pin took effect. Also fixes the cookie jar keyword in `test/Configuration.jl`, which was silently ignored as `jar` and therefore shared the global jar between the sub-testsets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xi94bDdW1Pgg85t4mCqXK
pankgeorg
marked this pull request as ready for review
October 4, 2026 18:34
This was referenced Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Another take on #3565: instead of migrating off HTTP.jl 1.x, support both 1.x and 2.x.
2.4 is the floor on the 2.x line because 16 advisories were all fixed in 2.4.0.
Why both
HTTP.jl 2 is a big breaking release (transport moved to Reseau,
Headersand message bodies became their own types,listen!changed shape), but the surface Pluto actually touches differs in only a handful of places. Supporting both means Pluto doesn't force the version choice on anyone sharing an environment with it, and gives the ecosystem room to move at its own pace.How
All version-dependent code lives in one new file,
src/webserver/HTTPCompat.jl, behind aHTTP_IS_V2compile-time constant. That's the whole list of differences:Sockets.TCPServerTCP.ListenerOrigincheckVector{UInt8}BytesBody/EmptyBodyContent-LengthBase.IOErrorBase.SystemErrorreadtimeoutread_idle_timeoutEverything else is written once, and
run!came out shorter than before:HTTP.streamhandler(app)— both versions ship it, and it does exactly what Pluto was doing by hand (read body → call handler → assign response → write body).Content-Length,Referrer-Policy,Server) and the Bindertokencapture moved into middleware, so they apply on both without touching the stream.on_shutdownis now called byBase.close(::RunningPlutoServer)rather than being a HTTP.jl 1listen!hook (2.x has no such hook). Ordering is preserved: both versions close the server gracefully, i.e. they wait for open connections to finish — including the WebSockets thaton_shutdownis what closes.iswritable(::HTTP.Stream)is gone in 2.x; replaced by a flag set inside the upgrade handler.On the
OrigincheckHTTP.jl 2 rejects a WebSocket upgrade whose
Origindoesn't match the request's own scheme, host and port. Pluto is routinely served through reverse proxies that terminate TLS and/or rewriteHost(Binder, JupyterHub, JuliaHub), which makes that comparison fail for perfectly legitimate clients. We opt out, matching HTTP.jl 1, which doesn't check the origin at all.This isn't a security regression: Pluto authenticates an upgrade with its session secret, and the cookie carrying that secret is
SameSite=Strict, so it is not sent along with a cross-site handshake.Tests
New
HTTP.jl N.xtestset intest/webserver.jlthat exercises precisely the parts that differ between the two:Content-Lengthis present andTransfer-Encodingis absent (this one fails on 1.x without the shim, which falls back to chunked)POST /notebookupload, the only route that doesconnect→👋, carrying a cross-siteOriginheaderclose(server)shuts down the notebooks it was runningTest.ymlgains four jobs that pin HTTP.jl 1.x (Pkg.add+Pkg.pin) across Linux/macOS/Windows; the default jobs resolve freely and get the latest 2.x. The testset asserts viaPLUTO_TEST_HTTP_VERSIONthat the pin actually took effect, so a silent resolution drift fails loudly instead of quietly testing 2.x twice.Also fixes
test/Configuration.jl:request(url, method; cookies=true, jar)was a silently ignored keyword on 1.x, so those sub-testsets were sharing HTTP.jl's global cookie jar instead of their own. It'scookiejar=jarnow — correct on 1.x, required on 2.x.Verified locally (Julia 1.12, HTTP 1.11.0 and 2.6.6)
webserver.jl46/46,Dynamic.jl34/34,Configuration.jl182/182 — on both.bonds.js › should not rerun bond values when refreshing pageis flaky on a cold server — it fails on whichever suite runs first and passes on re-run, identically on both versions.Note
Unlike #3565 I left
test/compiletimes.jl'sHTTP.get("http://github.com")alone — following the redirect works fine under 2.6.6, soredirect=falsedidn't seem needed.🤖 Generated with Claude Code
https://claude.ai/code/session_018xi94bDdW1Pgg85t4mCqXK
Try this Pull Request!
Open Julia and type: