From 13d50d3e60ff526d5c7ff9d3d9a0d224a42e177f Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:17:02 -0500 Subject: [PATCH] net/http: dispatch the client through its RoundTripper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client called the package-level dialing roundTrip directly, so a Client with no explicit Transport never reached the build-tagged Transport.RoundTrip — on js/wasm every request died with "Netdev not set" instead of using the fetch API (#66). Client.Do also short-circuited an explicit Transport past the cookie jar, header initialization, and timeout handling. send now takes a RoundTripper, Client.transport() falls back to DefaultTransport as documented, and Do always goes through do/send. The netdev path is unchanged: on !js targets DefaultTransport.RoundTrip is the same package-level roundTrip as before. Verified on js/wasm under node: http.Get with a nil Transport now reaches the fetch transport (its no-fetch-API report) instead of "Netdev not set". --- http/client.go | 23 +++++++----- http/clienttransport_test.go | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 http/clienttransport_test.go diff --git a/http/client.go b/http/client.go index 6f6589e..16e9a1d 100644 --- a/http/client.go +++ b/http/client.go @@ -158,7 +158,7 @@ func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTime req.AddCookie(cookie) } } - resp, didTimeout, err = send(req, deadline) + resp, didTimeout, err = send(req, c.transport(), deadline) if err != nil { return nil, didTimeout, err } @@ -177,11 +177,16 @@ func (c *Client) deadline() time.Time { return time.Time{} } +func (c *Client) transport() RoundTripper { + if c.Transport != nil { + return c.Transport + } + return DefaultTransport +} + // send issues an HTTP request. // Caller should close resp.Body when done reading from it. -func send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { - - // TINYGO: Removed round tripper +func send(req *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { if req.URL == nil { req.closeBody() @@ -192,6 +197,10 @@ func send(req *Request, deadline time.Time) (resp *Response, didTimeout func() b req.closeBody() return nil, alwaysFalse, errors.New("http: Request.RequestURI can't be set in client requests") } + if rt == nil { + req.closeBody() + return nil, alwaysFalse, errors.New("http: no Client.Transport or DefaultTransport") + } // TINYGO: Removed forkReq stuff @@ -208,7 +217,7 @@ func send(req *Request, deadline time.Time) (resp *Response, didTimeout func() b req.Header.Set("Authorization", "Basic "+basicAuth(username, password)) } - resp, err = roundTrip(req) + resp, err = rt.RoundTrip(req) if err != nil { // TINYGO: Remove TLS error check @@ -440,10 +449,6 @@ func urlErrorOp(method string) string { // Any returned error will be of type [*url.Error]. The url.Error // value's Timeout method will report true if the request timed out. func (c *Client) Do(req *Request) (*Response, error) { - if c.Transport != nil { - return c.Transport.RoundTrip(req) - } - return c.do(req) } diff --git a/http/clienttransport_test.go b/http/clienttransport_test.go new file mode 100644 index 0000000..45a92e3 --- /dev/null +++ b/http/clienttransport_test.go @@ -0,0 +1,72 @@ +package http + +import ( + "io" + "strings" + "testing" +) + +type recordingTransport struct { + calls int + req *Request +} + +func (t *recordingTransport) RoundTrip(req *Request) (*Response, error) { + t.calls++ + t.req = req + return &Response{ + Status: "200 OK", + StatusCode: 200, + Header: Header{}, + Body: io.NopCloser(strings.NewReader("ok")), + Request: req, + }, nil +} + +// A client with no Transport must dispatch through DefaultTransport, whose +// RoundTrip is build-tagged: the fetch API on js/wasm, the netdev dial +// elsewhere. Dispatching to the package-level dialer directly is issue #66. +func TestClientUsesDefaultTransport(t *testing.T) { + previous := DefaultTransport + defer func() { DefaultTransport = previous }() + rt := &recordingTransport{} + DefaultTransport = rt + + c := &Client{} + resp, err := c.Get("http://example.com/") + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if rt.calls != 1 { + t.Fatalf("DefaultTransport.RoundTrip calls = %d, want 1", rt.calls) + } + if rt.req.URL.Host != "example.com" { + t.Fatalf("request URL = %v", rt.req.URL) + } +} + +func TestClientUsesExplicitTransport(t *testing.T) { + rt := &recordingTransport{} + c := &Client{Transport: rt} + resp, err := c.Get("http://example.com/") + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + if rt.calls != 1 { + t.Fatalf("Transport.RoundTrip calls = %d, want 1", rt.calls) + } +} + +func TestClientNoTransport(t *testing.T) { + previous := DefaultTransport + defer func() { DefaultTransport = previous }() + DefaultTransport = nil + + c := &Client{} + _, err := c.Get("http://example.com/") + if err == nil || !strings.Contains(err.Error(), "no Client.Transport or DefaultTransport") { + t.Fatalf("error = %v, want no-transport error", err) + } +}