feat(grpc-web): Pyodide/WASM grpc-web transport for the async client - #2142
feat(grpc-web): Pyodide/WASM grpc-web transport for the async client#2142g-despot wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Secrets | View in Orca |
Lets the async client run inside Pyodide/WebAssembly, where grpcio has no wheel and sockets do not exist: gRPC goes over grpc-web (fetch) and REST over the browser's fetch, against Weaviate core's native /v1/grpc-web endpoint (1.38.3+). - packages/web: the weaviate-client-web companion distribution (pure-Python grpc shim, GrpcWebChannel, grpc-web framing, pyfetch/httpx senders, httpx-over-fetch transport) - weaviate/__init__.py: a bare `import weaviate` bootstraps the companion under Emscripten; a missing companion raises an install hint - setup.cfg: grpcio is skipped under Emscripten - connect/base.py: grpc_path_prefix on ConnectionParams (port collision allowed with a prefix), fail-fast check at client construction for a sync client or a missing shim, grpc-web.path_prefix channel option for the shim - connect/helpers.py: under Emscripten the async helpers pin gRPC to the REST endpoint under /v1/grpc-web (Con006 when a caller's gRPC endpoint is discarded); unchanged elsewhere - connect/v4.py: sync client rejected at construction under Emscripten; the gRPC ping error is passed through so WeaviateGRPCUnavailableError can name the real cause (grpc-web branch without firewall/port advice; 404 names server < 1.38.3 or a wrong prefix); fetch failures of the pypi version check are ignored - collections/batch/async_.py: batch.stream() fails fast under grpc-web, pointing to insert_many() - embedded.py: explicit error under Emscripten; proto/v1: grpcio version fallback when dist metadata is absent (Emscripten only), drift-pinned by proto_test - CI: grpc-web package tests (3.10-3.14) and a real-Pyodide e2e job under Node Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUNU7QgDr9MmFZnjKY9zFN
0b58414 to
21dc4b5
Compare
6705ab1 to
cc13ad9
Compare
Comment/docstring wording only, no code changes: shorter sentences, plainer words (shim -> replacement, honour -> use, multiplexed -> shares, discarded -> ignored, REST listener -> REST endpoint), same meaning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUNU7QgDr9MmFZnjKY9zFN
There was a problem hiding this comment.
Pull request overview
Adds Pyodide/WASM support to the async client through a companion grpc-web and browser-fetch transport.
Changes:
- Adds the
weaviate-client-webgrpc-web transport and shim. - Integrates automatic WASM routing, diagnostics, and unsupported-feature guards.
- Adds unit and Pyodide end-to-end CI coverage.
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
weaviate/warnings.py |
Adds endpoint-override warning. |
weaviate/proto/v1/__init__.py |
Adds Emscripten grpcio fallback. |
weaviate/exceptions.py |
Adds grpc-web diagnostics. |
weaviate/embedded.py |
Rejects embedded mode under WASM. |
weaviate/connect/v4.py |
Integrates grpc-web lifecycle and errors. |
weaviate/connect/helpers.py |
Routes async helpers through grpc-web. |
weaviate/connect/base.py |
Adds grpc-web connection parameters. |
weaviate/collections/batch/async_.py |
Rejects streaming batches over grpc-web. |
weaviate/__init__.py |
Bootstraps the companion package. |
test/test_wasm_compat.py |
Tests WASM-specific behavior. |
test/test_connection_params.py |
Tests grpc-web connection configuration. |
setup.cfg |
Excludes grpcio under Emscripten. |
pyrightconfig.json |
Type-checks the web package. |
proto_test/test_proto.py |
Tests grpcio fallback compatibility. |
packages/web/tests/test_transport.py |
Tests grpc-web transport behavior. |
packages/web/tests/test_single_import.py |
Tests automatic bootstrap. |
packages/web/tests/test_shim_install.py |
Tests shim installation. |
packages/web/tests/test_httpx_fetch.py |
Tests fetch-based REST transport. |
packages/web/tests/test_framing.py |
Tests grpc-web framing. |
packages/web/tests/conftest.py |
Configures package test imports. |
packages/web/src/weaviate_client_web/py.typed |
Marks the package as typed. |
packages/web/src/weaviate_client_web/_shim.py |
Implements the grpc API shim. |
packages/web/src/weaviate_client_web/_sender.py |
Implements HTTP senders. |
packages/web/src/weaviate_client_web/_httpx_fetch.py |
Implements fetch-backed HTTPX transport. |
packages/web/src/weaviate_client_web/_framing.py |
Implements grpc-web framing. |
packages/web/src/weaviate_client_web/_channel.py |
Implements the grpc-web channel. |
packages/web/src/weaviate_client_web/__init__.py |
Exposes and bootstraps the package. |
packages/web/README.md |
Documents usage and limitations. |
packages/web/pyproject.toml |
Defines companion package metadata. |
ci/pyodide-e2e/run.mjs |
Runs tests inside Pyodide. |
ci/pyodide-e2e/package.json |
Pins the Pyodide runtime. |
ci/pyodide-e2e/e2e.py |
Exercises WASM client workflows. |
.gitignore |
Ignores web-package build artifacts. |
.github/workflows/main.yaml |
Adds grpc-web and Pyodide CI jobs. |
Suppressed comments (1)
weaviate/connect/helpers.py:538
- This value comparison cannot tell an omitted argument from an explicitly supplied
grpc_port=50051(which the helper's own examples use). Under WASM that explicitly requested endpoint is still replaced by the REST port, but Con006 is suppressed, contrary to the documented promise that caller-supplied ports are warned about. Preserve argument presence with a sentinel default, then resolve the omitted value to 50051.
grpc_chosen_by_caller=grpc_port != _LOCAL_GRPC_PORT_DEFAULT,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…on006 - _encode_timeout/_abort_signal_ms: a huge finite timeout (e.g. 1e308) overflowed to infinity in the millisecond multiplication and raised OverflowError; both now compare against their cap before multiplying (no deadline / capped, as documented) - the "server too old / wrong grpc-web path" diagnosis now also requires the channel's synthetic "HTTP 404"/"HTTP 405" marker in the details, so a genuine UNIMPLEMENTED from a routed endpoint gets the generic message instead; the string contract is noted on both sides - Con006 prints grpc:// / grpcs:// schemes on both endpoints, so a mismatch in grpc_secure alone no longer shows two identical strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUNU7QgDr9MmFZnjKY9zFN
| async def _fetch_handle_async_request( | ||
| self: httpx.AsyncHTTPTransport, request: httpx.Request | ||
| ) -> httpx.Response: | ||
| from pyodide.http import pyfetch # type: ignore[import-not-found] |
There was a problem hiding this comment.
Why is this import within the function instead of at the top of the file?
| ``pyodide`` does not exist). ``pyfetch`` has no timeout parameter of its own; the | ||
| call deadline is enforced by ``GrpcWebChannel._unary`` via ``asyncio.wait_for``. | ||
| """ | ||
| from pyodide.http import pyfetch # type: ignore[import-not-found] |
There was a problem hiding this comment.
Ah I see it's so that the import is lazy
This makes me wonder, should we instead provide grpc-web functionality using the extras syntax, i.e.
pip install weaviate-client[grpc-web]
so that pyodide and cpython users don't conflict?
The default installation pip install weaviate-client would ship as it is right now and the extras would bundle this extra weaviate_client_web dep into it. wdyt?
What & why
Lets the async client run inside Pyodide/WebAssembly (marimo notebooks, browser, WASM workers), where
grpciohas no wheel and sockets don't exist. gRPC is re-routed over grpc-web (fetch), REST over the browser'sfetch— against Weaviate core's native/v1/grpc-webendpoint (default-on since 1.38.3).This is the trimmed successor of #2056 (kept open for reference): only what the WASM goal actually needs. The general fixes that were bundled there now ship separately:
fix/token-refresh-lifecycle): the async OIDC refresher must be an asyncio task because threads cannot start under Pyodide.Quickstart (Pyodide / marimo)
Under Emscripten all three async helpers (
use_async_with_local/_weaviate_cloud/_custom) pin gRPC to the REST endpoint under/v1/grpc-webthemselves — the same contract as the TS@weaviate/webclient'swebify(). There is nogrpc_path_prefixparameter on the helpers: pass gRPC arguments equal to the HTTP ones; anything else is discarded with aCon006warning. A grpc-web transcoder on a separate endpoint needs hand-builtConnectionParams(..., grpc_path_prefix=...). Off Emscripten nothing changes: the helpers build exactly the params they always did (pinned bytest_helper_params_off_emscripten_are_unchanged).Key pieces
packages/web/— companion distributionweaviate-client-web(unchanged from #2056):_shim.py(pure-Pythongrpcmodule shim),_channel.py(GrpcWebChannel: unary calls, metadata → headers, grpc-web status →AioRpcError,grpc-timeoutencoding, 404/405 diagnostics),_framing.py,_sender.py(pyfetch, plus an httpx sender for CPython testing),_httpx_fetch.py(REST over JSfetch), README.Base client (no-ops off Emscripten):
weaviate/__init__.py— platform-guarded bootstrap: a bareimport weaviatesoft-imports the companion; a missing companion raises an install hint.setup.cfg—grpcio ; sys_platform != "emscripten".connect/base.py—grpc_path_prefixonConnectionParams(port collision allowed with a prefix);_check_grpc_web_usableruns once, at client construction (sync client or missing shim fails fast);grpc-web.path_prefixchannel option for the shim.connect/helpers.py—_webify(): the async helpers route gRPC to the REST endpoint under Emscripten; Con006 when a caller's gRPC endpoint is discarded.connect/v4.py— sync client rejected at construction under Emscripten (a 3-line check in_ConnectionBase.__init__);_ping_grpcpasses the gRPC error through so the diagnostics can say what went wrong;_check_package_versionignoresOSError(fetch blocked by a page CSP).exceptions.py—WeaviateGRPCUnavailableErrorgains the gRPC code/details and a grpc-web branch (no firewall/port advice; a 404 names the two real causes: server < 1.38.3 or a wrong prefix).collections/batch/async_.py—batch.stream()fails fast under grpc-web, pointing toinsert_many()(bidi streaming is impossible over fetch).embedded.py— explicit error under Emscripten.proto/v1/__init__.py— grpcio version fallback when dist metadata is absent (Emscripten only), drift-pinned byproto_test.Dropped vs #2056 (and why)
_deadline/_Deadlinesplumbing acrossv4.pyand both batch modules: the web package already treats non-finite timeouts as "no deadline" in both transports (_encode_timeout,_abort_signal_ms); the one CPython effect (Thread.join(inf)in the sync batch-stream shutdown wait) is a separate one-line fix, not included here.ConnectionSync.__init__signature override (→ 3 lines in the base constructor) and the duplicate channel-level_check_grpc_web_usablecall.test_no_helper_takes_a_grpc_path_prefix, the Emscripten__get_timeouttest, the channel-level reject pair, the trivial platform-passes tests, …).CI / tests
grpc-web-tests(package suite, 3.10–3.14) andpyodide-e2e(real Pyodide under Node against core-native/v1/grpc-web, Weaviate 1.39.0) jobs; ruff / flake8 / pyright coverpackages/web;build-and-publishneeds both. Base-client tests:test/test_wasm_compat.py,test/test_connection_params.py,proto_test. Locally: 176 tests across those +packages/web/testspass; pyright clean for the whole project.Deferred
Publish/lockstep versioning for
weaviate-client-web; once on PyPI,weaviate-clientgainsweaviate-client-web ; sys_platform == "emscripten", collapsing the WASM install tomicropip.install("weaviate-client").🤖 Generated with Claude Code
https://claude.ai/code/session_01GUNU7QgDr9MmFZnjKY9zFN