Skip to content

net/http: expose dialing errors from net package #23827

@szuecs

Description

@szuecs

I would like to get meaningful errors from the net package. Right now I can not decide if I got an error while a connection is already established or if it's while creating a TCP or TLS handshake.
I want to be able to decide from an error, if I can retry the call or not. Possibly net.Error interface has to get a new function to decide this easily.

The code shows a workaround wrapping a net.Dialer to decide if the error happened was during Dial or an already established connection.

What version of Go are you using (go version)?

go version go1.9.4 linux/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

    GOARCH="amd64"
    GOBIN="/home/sszuecs/go/bin"
    GOEXE=""
    GOHOSTARCH="amd64"
    GOHOSTOS="linux"
    GOOS="linux"
    GOPATH="/home/sszuecs/go"
    GORACE=""
    GOROOT="/usr/share/go"
    GOTOOLDIR="/usr/share/go/pkg/tool/linux_amd64"
    GCCGO="gccgo"
    CC="gcc"
    GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build480041304=/tmp/go-build -gno-record-gcc-switches"
    CXX="g++"
    CGO_ENABLED="1"
    CGO_CFLAGS="-g -O2"
    CGO_CPPFLAGS=""
    CGO_CXXFLAGS="-g -O2"
    CGO_FFLAGS="-g -O2"
    CGO_LDFLAGS="-g -O2"
    PKG_CONFIG="pkg-config"

What did you do?

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/http"
	"time"
)

type proxyError struct {
	err           error
	code          int
	dialingFailed bool
}

func (e proxyError) Error() string {
	return fmt.Sprintf("proxyError: %v", e.err)
}

type myDialer struct {
	net.Dialer
	f func(ctx context.Context, network, addr string) (net.Conn, error)
}

func myDialerNew(d net.Dialer) *myDialer {
	return &myDialer{
		Dialer: d,
		f:      d.DialContext,
	}
}

func (dc *myDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	con, err := dc.f(ctx, network, addr)
	if err != nil {
		return nil, proxyError{
			err:           err,
			dialingFailed: true,
		}
	}
	return con, nil
}

func main() {

	tr := &http.Transport{
		DialContext: myDialerNew(net.Dialer{
			Timeout:   100 * time.Millisecond,
			KeepAlive: 30 * time.Second,
			DualStack: true,
		}).DialContext,
		TLSHandshakeTimeout:   200 * time.Millisecond,
		ResponseHeaderTimeout: 500 * time.Millisecond,
		IdleConnTimeout:       5 * time.Second,
	}

	go func(rt http.RoundTripper) {
		for {
			time.Sleep(1 * time.Second)

			req, err := http.NewRequest("GET", "http://127.0.0.1:10000/", nil)
			if err != nil {
				log.Printf("Failed to create request: %v", err)
				continue
			}

			resp, err := rt.RoundTrip(req)
			if err != nil {
				if perr, ok := err.(proxyError); ok {
					if nerr, ok := perr.err.(interface {
						Temporary() bool
						Timeout() bool
					}); ok {
						log.Printf("Failed to do roundtrip %v %v: %v", nerr.Temporary(), nerr.Timeout(), perr.err)
					}
					log.Printf("dial error: %v", perr)
				} else {
					if nerr, ok := err.(interface {
						Temporary() bool
						Timeout() bool
					}); ok {
						log.Printf("Failed to do roundtrip %v %v: %v", nerr.Temporary(), nerr.Timeout(), err)
					}
					log.Printf("not a dial error: %v", err)
				}
				continue
			}
			resp.Body.Close()
			log.Printf("resp status: %v", resp.Status)
		}
	}(tr)

	ch := make(chan struct{})
	<-ch
}

What did you expect to see?

I would like to do:

if nerr, ok := err.(net.Error); ok {
    if nerr.DialError() {
        retrySend(req)
    }
}

What did you see instead?

net.Error has Temporary() and Timeout() defined and rest of the errors are "untyped" fmt.Errorf("connection refused").

I would like to be able to dispatch by retrieable error. In an http proxy/loadbalancer this means you want to decide if you sent data or not, because if you already sent data, you should not retry.
I have to built a workaround as shown above in zalando/skipper@d3fa9e9 .

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions