Description
Net::FTP has no #initialize: construction is wired to .new and to nothing else
Measured against MRI 4.0.6 (net-ftp 0.3.9) over all 56 library/net-ftp files,
same corpus, 30 s per file:
| engine | passing |
|---|---|
| MRI 4.0.6 | 459 |
| rbgo | 331 |
| −128 |
initialize_spec.rb alone is −38 of that, and it is the largest single item
left in the module: MRI passes 41, rbgo passes 3.
The cause
Net::FTP#initialize is not defined. Construction lives entirely in the
class method Net::FTP.new, so the class inherits Object#initialize, which
takes no arguments:
RBGO_RESULT pass=3 fail=2 error=36 skip=0
RBGO_DETAIL error ArgumentError Net::FTP#initialize when passed host | tries to connect to the passed host
wrong number of arguments (given 1, expected 0)
ruby/spec drives initialize directly, which is normal for this module —
it is how the suite tests construction without connecting:
@ftp.send(:initialize, nil, options)
@ftp.passive.should == falseand it asserts the method is there and private:
it "is private" do
Net::FTP.private_instance_methods(false).should.include?(:initialize)
endThe shape
This is the defect this repository keeps producing: a mechanism wired at only
some of its doors. Construction works perfectly through .new and does not
exist through #initialize, which is the door MRI documents as a private
instance method and which ruby/spec uses throughout.
It is the tenth instance recorded this week, after ioBlock in Net::HTTP but
not Net::FTP, read_timeout read from the options Hash but not
open_timeout, and ftpInit returning before the options when the host was
nil.
Shape of the fix
Define Net::FTP#initialize(host = nil, options = {}) as a private instance
method carrying what ftpInit does today, and make Net::FTP.new allocate
and call it. Then .new, .open and send(:initialize, …) all go through one
implementation instead of one door being a special case.
Worth checking while there: MRI calls options.to_hash on the argument, so an
object that converts counts as an options Hash; rbgo currently requires a
literal Hash.
Not a hang
For the record, since this module is cited in #916: with a 30 s per-file limit,
no file is killed by the clock under either engine — specdelta prints no
KILLED on timeout row in this sweep. The remaining −128 is ordinary
unimplemented behaviour, not the hang symptom.
Prior art: priorart "Net::FTP#initialize private instance method send" at 2026-11-13 14:40 CET reported prior art; checked: priorart --repo go-embedded-ruby/ruby 'Net::FTP#initialize private instance method send' returned 508 broad keyword matches; the nearest are #916 (the net-ftp hang issue, which this explicitly is NOT), #928 (my own open PR, which adds default_passive and fixes ftpInit dropping options on a nil host but does not define #initialize), and #621 (an mspec shim gap). None covers Net::FTP#initialize being undefined.
Activity
tannevaled commentedon Nov 13, 2026
Before implementing this:
Net::FTP.allocatedoes not produce aNet::FTPDefining
#initializeis necessary and not sufficient, because of how thespec reaches it:
Measured on
main:allocatehands back a generic*RObjectthat merely reports the classNet::FTP. Every accessor then fails its type assertion, so an#initializedefined on top of it would be handed an object it cannot populate. The 38
examples in
initialize_spec.rball start from thatallocate.So the work is larger than "define a private
#initialize":class itself provides, and
Class#allocatehas to consult it instead ofbuilding a plain
RObject;#initializecan be a private instance method carrying whatftpInitdoes today, with
.newbecoming allocate-then-initialize;send(:initialize, nil, options)reach a realftpObj.Step 1 is not specific to
Net::FTP— every native-backed class has the samehole, so it wants doing once in the object model rather than per binding. That
is a different and wider change than this issue was filed for, and worth saying
before someone starts at step 2.
⛔ A separate, smaller defect found on the way
The failure above is a Go interface-conversion message reaching Ruby:
Internal type names have no business in a user-visible exception, and the class
is wrong too — MRI would raise
NoMethodErrororTypeErrorhere, notArgumentError. Whatever happens to the rest of this issue, that message isworth not shipping: it tells an embedder about
*vm.ftpObj, and it will beproduced by any native-backed class reached through
allocate.