Repository navigation
feat(client): add Hrana 3 JSON over WebSocket transport - #190
Merged
Merged
Conversation
Add the WebSocket envelope on top of the existing leaf structures: sealed WsClientMsg/WsRequest/WsServerMsg/WsResponse records and an HranaWsCodec SPI implemented by DirectHranaJsonCodec. Wire format follows the Hrana 3 specification text frames, including null JWTs and request_id/response type matching.
Introduce LibsqlClient as the transport-independent client used by the JDBC driver, move the HTTP interactive baton state into LibsqlHttpClient, and add LibsqlWebSocketClient speaking Hrana 3 JSON with the hrana3 subprotocol. The WebSocket is implemented over Socket/SSLSocket rather than java.net.http.WebSocket: sqld answers the upgrade with a 101 carrying content-length: 31 and no body, and the JDK handshake waits for a full response body, so it never completes against any sqld version.
…ient LibsqlUrlParser now keeps ws/wss schemes (including jdbc:sqlite: forms) and maps tls=true to wss. LibsqlDriver picks LibsqlWebSocketClient for those URLs. LibsqlConnection works only through the LibsqlClient seam, so auto-commit paths use one-shot calls and transactions use the interactive stream.
Run selects, commit/rollback, failing statements, batches and metadata against jdbc:libsql:ws:// on the pinned sqld container, and check that ws/wss URLs parse with the expected scheme and path.
magnusp
added a commit
that referenced
this pull request
Oct 24, 2026
Add the Protobuf encoding for Hrana 3 over WebSocket (`hrana3-protobuf` subprotocol, binary frames). JSON stays the default. A URL of `jdbc:libsql:ws://host:port?encoding=protobuf` selects it. Fixes #188. This PR is stacked on #190 and targets `feat/i187-hrana-ws-json`. Merge #190 first. ## What changes - `HranaWsProtobufCodec` implements `HranaWsCodec` over the already vendored `hrana_ws.proto` and `hrana.proto` schemas. It maps `Stmt`, `Value`, `Batch`, `BatchCond`, `StmtResult`, and `BatchResult` both ways. - `LibsqlClientConfig` gains an `Encoding` enum (`JSON`, `PROTOBUF`) as its last component. The existing constructors still default to JSON. - `LibsqlUrlParser` reads `encoding=json|protobuf|proto`. An unknown value throws, and `encoding=protobuf` on an `http` or `https` URL throws because Protobuf over HTTP is out of scope. - `LibsqlWebSocketClient` derives the subprotocol from the codec, `hrana3` for JSON and `hrana3-protobuf` for Protobuf, and rejects a server that negotiates a different one. - `LibsqlDriver` selects the codec from the encoding. No new dependency. `protobuf-java` and the codegen were already present. ## Evidence - `./mvnw -q -pl libsql-hrana-codec,libsql-client,libsql-jdbc -am test` passes. - `./mvnw -q spotless:check` passes. - `DOCKER_CONFIG="$PWD/libsql-conformance/src/test/docker-config" ./mvnw -pl libsql-itests -am -Dit.test='WebSocket*TransportIT' -DfailIfNoTests=false verify` passes 8 of 8, split as `WebSocketTransportIT` 5 of 5 on JSON and `WebSocketProtobufTransportIT` 3 of 3 on Protobuf. The Protobuf integration test covers typed select values including `Long.MIN_VALUE` and a blob, transaction commit and rollback, and error mapping. The codec unit test covers `Long.MIN_VALUE`, `Long.MAX_VALUE`, doubles, unicode text, null, empty and non-empty blobs, named arguments, batch conditions, and both error messages. ## Limits - Protobuf over HTTP (`v3-protobuf/pipeline`) is out of scope. - `wss` has no TLS integration test. The container runs plain `ws`. - Cursor requests are not implemented. Nothing in the driver uses them.
magnusp
added a commit
that referenced
this pull request
Oct 24, 2026
…d-rolled framer (#193) Replace the hand-rolled RFC 6455 transport inside `LibsqlWebSocketClient` with Apache HttpClient's `httpclient5-websocket` module. The WebSocket transport now reuses a library that already does framing, handshake, TLS, and fragmentation, and about 380 lines of that code leave the client. Fixes #192. ## Why sqld answers the upgrade with a `101` that carries `content-length: 31` and no body, so the JDK client's `java.net.http.WebSocket` never finishes its opening handshake. #190 shipped a hand-written framer to work around that. HttpClient treats a `101` as informational and upgrades without reading a body, so the workaround is no longer needed. ## What changes - `libsql-client` depends on `httpclient5-websocket` 5.7-beta1, pinned in the root pom. `LibsqlWebSocketClient` keeps its `request_id` matching, stream ids, and blocking call semantics on top of the library. - The `Transport` inner class, the static handshake, the frame encoder and decoder, the accept-key computation, and the socket and `SSLSocket` setup are deleted. - Closing a connection tears the client down in one step, so close latency and thread lifetime stay what the hand-rolled transport gave: single-digit milliseconds and no leftover IO threads. - Both WebSocket integration tests now run every case over `ws://` and `wss://`. The wss path goes through an nginx TLS terminator in front of sqld with a self-signed localhost certificate that the failsafe JVM trusts. ## Scope - JSON and Protobuf over `ws` and `wss`. No Hrana message, JDBC API, or HTTP transport change. - `permessage-deflate` is off, so the bytes on the wire stay what they were. - Cursor requests stay unimplemented, as in #190. ## Tradeoffs - 5.7 is beta, so the version is pinned exactly and the adapter stays inside one class. - The dependency brings `httpclient5`, `httpcore5`, `httpcore5-h2`, `httpclient5-cache`, and `slf4j-api` into `libsql-client`. An application with no SLF4J binding gets SLF4J's one-time "no providers" notice on stderr. - The hand-rolled transport is deleted rather than kept behind a flag, on your call. ## Blast Radius Every `jdbc:libsql:ws://` and `wss://` connection changes transport. The HTTP transport, the codec, and the JDBC layer are untouched, and their suites still pass. The hidden cost is dependency footprint for anyone who only uses `http://` URLs but resolves the client module's transitive dependencies. ## Verification - `./mvnw spotless:check` passes, and `./mvnw test` passes (374 JDBC tests, 35 codec tests). - `WebSocketTransportIT` and `WebSocketProtobufTransportIT` run 15 cases across `ws://` and `wss://` against `ghcr.io/tursodatabase/libsql-server:v0.24.33` and pass. - `mvn clean verify` passes locally, including the HTTP integration tests, the Hikari pool churn test, the toxiproxy test, and the conformance suite.
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.
Add a WebSocket transport that speaks Hrana 3 JSON (
hrana3subprotocol) to sqld. A JDBC URL ofjdbc:libsql:ws://host:portorjdbc:libsql:wss://host:portnow uses it. The HTTP transport is unchanged.Fixes #187.
What changes
libsql-hrana-codecgains the WebSocket envelope models (WsClientMsg,WsServerMsg,WsRequest,WsResponse) and theHranaWsCodecSPI.DirectHranaJsonCodecimplements the JSON envelope, reusing its existing leaf serializers.libsql-clientgains aLibsqlClientseam.LibsqlHttpClientimplements it, andLibsqlWebSocketClientis the new transport.LibsqlConnectionnow depends on the seam instead of the HTTP client and the baton.libsql-jdbcacceptswsandwssURLs and selects the client from the scheme.Why a seam is required
LibsqlConnectionused the HTTP baton as the single source of truth for whether a stream is open. WebSocket has no baton, because the client assigns astream_id. The client now owns the stream state and exposeshasOpenStream(), so the lazy transaction begin keeps working on both transports.WebSocket implementation note
The client speaks RFC 6455 directly over a socket.
java.net.http.WebSocketis not used because the JDK opening handshake waits for a response body that sqld never sends. A JDK probe reproduces the hang against the pinned image, while a raw WebSocket client completes the same handshake.Evidence
./mvnw -q -pl libsql-hrana-codec,libsql-client,libsql-jdbc -am testpasses../mvnw -q spotless:checkpasses.DOCKER_CONFIG="$PWD/libsql-conformance/src/test/docker-config" ./mvnw -pl libsql-itests -am -Dit.test=WebSocketTransportIT -DfailIfNoTests=false verifypasses 5 of 5 againstghcr.io/tursodatabase/libsql-server:v0.24.33.libsql-itests verifysuite passed before the field-order fix. That fix only changes the WebSocket codec.The integration test covers typed select values, transaction commit and rollback on one stream, error mapping, metadata, batch execution, and URL parsing.
Limits
wsshas no TLS integration test. The container runs plainws.open_cursor,fetch_cursor) are not implemented. Nothing in the driver uses them.