Priority: P0
Related API contract: Provide a stable error hierarchy and safe, class-specific metadata
Describe the issue
After wreq-ruby has executed a request in a parent process, using it after Process.fork is unsafe. With wreq 1.2.4 on Ruby 3.3.11 (arm64-darwin25):
- an inherited
Wreq::Client failed in the child with Wreq::ConnectionError; and
- constructing a fresh
Wreq::Client in the child produced a native segmentation fault.
This matters to ordinary Ruby deployments. Prefork servers load application code in a master process and then fork workers. A native extension may decide that inherited clients cannot be reused, but it must detect that state and recover or raise a Ruby exception; it must never crash the process.
Reproduction
The following uses a local socket only. The first request initializes wreq-ruby's global Tokio runtime before the second fork:
require "socket"
require "wreq"
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server_pid = fork do
socket = server.accept
loop { break if socket.gets == "\r\n" }
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
socket.close
exit! 0
end
server.close
Wreq::Client.new.get("http://127.0.0.1:#{port}/").bytes
Process.wait(server_pid)
child_pid = fork do
Wreq::Client.new
exit! 0
end
_, status = Process.wait2(child_pid)
p status.signaled?
p status.termsig
Observed on the platform above:
[BUG] Segmentation fault
true
11
In a separate child, reusing the client that performed the warm-up request raised Wreq::ConnectionError instead of completing a local request.
Required behavior
- Detect when the current PID differs from the PID that owns the native runtime or client pool.
- Never reuse parent sockets, executors, tasks, or synchronization state in a child.
- A freshly constructed client must work in the child after the parent has used wreq-ruby.
- For an inherited client, either rebuild process-local resources lazily or raise a documented Ruby exception such as
Wreq::ForkError < Wreq::Error.
- If transparent reinitialization is not feasible, expose an idempotent
Wreq.after_fork! hook and document it for prefork servers. Automatic PID detection is preferable because Process.fork, IO.popen("-"), and server-specific hooks can all create children.
- Parent-process clients must continue to work after a child exits.
The exact implementation is intentionally not prescribed. The observable contract is that a PID change cannot lead to undefined behavior or a native abort.
Acceptance criteria
- No fork-related path can panic, abort, hang indefinitely, or segfault Ruby.
- A child can construct a new client and complete a local request after the parent has completed one.
- Inherited clients have explicit, tested semantics: safe reinitialization or a typed Ruby error.
- Runtime and connection-pool state is process-local after a fork.
- Tests run the scenario in subprocesses on platforms that support
fork and assert both exit status and request behavior.
- Documentation includes a Puma/Unicorn-style prefork example if an explicit hook is required.
Ruby ecosystem precedent
- Puma cluster mode forks worker processes and provides
before_fork/before_worker_boot hooks specifically for process-local resource management.
- Ruby documents
Process._fork as the common internal path used by Kernel#fork, Process.fork, and IO.popen("-"), and as a hook point for libraries that must observe fork events.
Relevant wreq-ruby source
Priority: P0
Related API contract: Provide a stable error hierarchy and safe, class-specific metadata
Describe the issue
After wreq-ruby has executed a request in a parent process, using it after
Process.forkis unsafe. With wreq 1.2.4 on Ruby 3.3.11 (arm64-darwin25):Wreq::Clientfailed in the child withWreq::ConnectionError; andWreq::Clientin the child produced a native segmentation fault.This matters to ordinary Ruby deployments. Prefork servers load application code in a master process and then fork workers. A native extension may decide that inherited clients cannot be reused, but it must detect that state and recover or raise a Ruby exception; it must never crash the process.
Reproduction
The following uses a local socket only. The first request initializes wreq-ruby's global Tokio runtime before the second fork:
Observed on the platform above:
In a separate child, reusing the client that performed the warm-up request raised
Wreq::ConnectionErrorinstead of completing a local request.Required behavior
Wreq::ForkError < Wreq::Error.Wreq.after_fork!hook and document it for prefork servers. Automatic PID detection is preferable becauseProcess.fork,IO.popen("-"), and server-specific hooks can all create children.The exact implementation is intentionally not prescribed. The observable contract is that a PID change cannot lead to undefined behavior or a native abort.
Acceptance criteria
forkand assert both exit status and request behavior.Ruby ecosystem precedent
before_fork/before_worker_boothooks specifically for process-local resource management.Process._forkas the common internal path used byKernel#fork,Process.fork, andIO.popen("-"), and as a hook point for libraries that must observe fork events.Relevant wreq-ruby source
src/rt.rssrc/gvl.rssrc/client.rs