Load the networking stack only when an import needs it - #30
Merged
Conversation
Adding ssrf_filter in 0.6.0 made `require 'cataract'` depend on the socket extension. Both backends required import_resolver at load time, and that file required open-uri and ssrf_filter at file scope, which pull in net/http and resolv. Parsing a string of CSS needed a networking stack, and the library would not load at all where sockets don't exist - WebAssembly/WASI being the case that matters. v0.5.0 loaded fine there. Three separate things reached for sockets: - both backends required import_resolver up front. Stylesheet#load_uri and #resolve_imports now require it on entry instead, which is every path that uses it. - import_resolver required open-uri and ssrf_filter at file scope, so option validation, URL normalization and file:// fetching couldn't be used without them - and neither could a caller-supplied fetcher, which is how a host with no sockets is meant to resolve imports at all. They load in DefaultFetcher#fetch_http now. - DefaultFetcher#call named OpenURI::HTTPError, SsrfFilter::Error and SocketError in rescue clauses. Ruby evaluates those constants when an exception passes through, so any failure raised NameError where the libraries were absent - including failures having nothing to do with the network. That translation moved down into #fetch_http with the requires. #call also rescued StandardError and re-raised as ImportError, which caught the ImportError it had itself raised a few lines earlier, so "Unsupported scheme: ftp" was reported as "Error fetching import: ftp://... (Cataract::ImportError: Unsupported scheme: ftp)". It now re-raises ImportError untouched. With this, on a host without sockets: the library loads, CSS parses and serializes, flatten works, @import statements parse and survive, and imports resolve either through a caller-supplied fetcher or from the filesystem. Only fetching over HTTP is unavailable, which is inherent. ci-wasm.yml runs test/wasm/smoke.rb against a real ruby.wasm build under wasmtime to check exactly that. It is workflow_dispatch only: it pulls a prebuilt Ruby, covers the pure backend alone, and answers a question that changes rarely. The script uses plain raises rather than minitest, so gem resolution isn't a second thing that can fail.
A failing worker raised "cataract benchmark failed" and nothing else. Its stderr was streamed to the console as it arrived, but by the time the exception surfaced - through a rake task, or a test that captures output - it was gone, leaving a failure with no cause and no way to tell which of the four variants to reproduce. run_subprocess now keeps stderr alongside stdout, and a failed variant reports the exit status, the full command with the backend/JIT/results variables that select it, and the tail of stderr. It distinguishes exiting from being killed, because those look identical otherwise and read very differently: cataract benchmark failed - killed by SIGSEGV cataract benchmark failed - exited 3 The signal case matters here: the native worker runs a C extension under Process.warmup, which compacts the heap, and a crash there produces no Ruby backtrace at all. Also drops three comments that documented the absence of a require rather than the behaviour of anything present.
Core carries Time#iso8601 from Ruby 4.0 on. Below that it comes from the time library, which nothing here required, so every benchmark run died on 3.3: undefined method `iso8601' for an instance of Time SystemMetadata.collect runs during setup for every benchmark, so this hit each of the four worker subprocesses rather than just the final write. The failure was invisible on 4.0, which is where the harness was developed. Also sends the harness's own failure report to stderr instead of stdout, and falls back to a worker's stdout when its stderr is empty. Both were in the way of finding this: the worker printed its cause to stdout, the parent captured only stderr, and the plumbing test wrapped the run in capture_io - so the message existed and was discarded three times over.
jamescook
marked this pull request as ready for review
July 31, 2026 22:38
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.
Adding ssrf_filter in 0.6.0 made
require 'cataract'depend on the socket extension. Both backends required import_resolver at load time, and that file required open-uri and ssrf_filter at file scope, which pull in net/http and resolv. Parsing a string of CSS needed a networking stack, and the library would not load at all where sockets don't exist - WebAssembly/WASI being the case that matters. v0.5.0 loaded fine there.Three separate things reached for sockets:
#call also rescued StandardError and re-raised as ImportError, which caught the ImportError it had itself raised a few lines earlier, so "Unsupported scheme: ftp" was reported as "Error fetching import: ftp://... (Cataract::ImportError: Unsupported scheme: ftp)". It now re-raises ImportError untouched.
With this, on a host without sockets: the library loads, CSS parses and serializes, flatten works, @import statements parse and survive, and imports resolve either through a caller-supplied fetcher or from the filesystem. Only fetching over HTTP is unavailable, which is inherent.
ci-wasm.yml runs test/wasm/smoke.rb against a real ruby.wasm build under wasmtime to check exactly that. It is workflow_dispatch only: it pulls a prebuilt Ruby, covers the pure backend alone, and answers a question that changes rarely. The script uses plain raises rather than minitest, so gem resolution isn't a second thing that can fail.