How can I implement idle timeout when loading http

I read the various timeouts that are available in the HTTP request, and all of them seem to act as tight deadlines for the duration of the request.

I am starting the http download, I do not want to implement a hard timeout after the initial handshake, since I do not know anything about connecting my users and do not want a timeout on slow connections. Ideally, I would like to disable the timeout after a period of inactivity (when nothing was loaded in x seconds). Is there a way to do this as an inline or do I need to interrupt based on the file statement?

The working code is a little difficult to isolate, but I think these are the relevant parts, there is another loop that defines the file to ensure progress, but I will need to reorganize the bit to use this to interrupt the download:

// httspClientOnNetInterface returns an http client using the named network interface, (via proxy if passed)
func HttpsClientOnNetInterface(interfaceIP []byte, httpsProxy *Proxy) (*http.Client, error) {

    log.Printf("Got IP addr : %s\n", string(interfaceIP))
    // create address for the dialer
    tcpAddr := &net.TCPAddr{
        IP: interfaceIP,
    }

    // create the dialer & transport
    netDialer := net.Dialer{
        LocalAddr: tcpAddr,
    }

    var proxyURL *url.URL
    var err error

    if httpsProxy != nil {
        proxyURL, err = url.Parse(httpsProxy.String())
        if err != nil {
            return nil, fmt.Errorf("Error parsing proxy connection string: %s", err)
        }
    }

    httpTransport := &http.Transport{
        Dial:  netDialer.Dial,
        Proxy: http.ProxyURL(proxyURL),
    }

    httpClient := &http.Client{
        Transport: httpTransport,
    }

    return httpClient, nil
}

/*
StartDownloadWithProgress will initiate a download from a remote url to a local file,
providing download progress information
*/
func StartDownloadWithProgress(interfaceIP []byte, httpsProxy *Proxy, srcURL, dstFilepath string) (*Download, error) {

    // start an http client on the selected net interface
    httpClient, err := HttpsClientOnNetInterface(interfaceIP, httpsProxy)
    if err != nil {
        return nil, err
    }

    // grab the header
    headResp, err := httpClient.Head(srcURL)
    if err != nil {
        log.Printf("error on head request (download size): %s", err)
        return nil, err
    }

    // pull out total size
    size, err := strconv.Atoi(headResp.Header.Get("Content-Length"))
    if err != nil {
        headResp.Body.Close()
        return nil, err
    }
    headResp.Body.Close()

    errChan := make(chan error)
    doneChan := make(chan struct{})

    // spawn the download process
    go func(httpClient *http.Client, srcURL, dstFilepath string, errChan chan error, doneChan chan struct{}) {
        resp, err := httpClient.Get(srcURL)
        if err != nil {
            errChan <- err
            return
        }
        defer resp.Body.Close()

        // create the file
        outFile, err := os.Create(dstFilepath)
        if err != nil {
            errChan <- err
            return
        }
        defer outFile.Close()

        log.Println("starting copy")
        // copy to file as the response arrives
        _, err = io.Copy(outFile, resp.Body)

        // return err
        if err != nil {
            log.Printf("\n Download Copy Error: %s \n", err.Error())
            errChan <- err
            return
        }

        doneChan <- struct{}{}

        return
    }(httpClient, srcURL, dstFilepath, errChan, doneChan)

    // return Download
    return (&Download{
        updateFrequency: time.Microsecond * 500,
        total:           size,
        errRecieve:      errChan,
        doneRecieve:     doneChan,
        filepath:        dstFilepath,
    }).Start(), nil
}

Update Thanks to everyone who contributed to this.

I accepted JimB's answer as it seems like a perfectly viable approach, which is more generalized than the solution I chose (and probably more useful for anyone who finds their way here).

In my case, I already had a loop controlling the file size, so I threw a named error if that didn't change in x seconds. It was much easier for me to pick up the named error using my existing error handling and retry the download from there.

, goroutine ( ), ( ), ( )

+4
1

. , , io ( ErrShortWrite, , std library io.Writer )

, , . , , goroutine .

func idleTimeoutCopy(dst io.Writer, src io.Reader, timeout time.Duration,
    ctx context.Context, cancel context.CancelFunc) (written int64, err error) { 
    read := make(chan int)
    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case <-time.After(timeout):
                cancel()
            case <-read:
            }
        }
    }()

    buf := make([]byte, 32*1024)
    for {
        nr, er := src.Read(buf)
        if nr > 0 {
            read <- nr
            nw, ew := dst.Write(buf[0:nr])
            written += int64(nw)
            if ew != nil {
                err = ew
                break
            }
        }
        if er != nil {
            if er != io.EOF {
                err = er
            }
            break
        }
    }
    return written, err
}

time.After , Timer. , reset, Reset:

    t := time.NewTimer(timeout)
    for {
        select {
        case <-ctx.Done():
            return
        case <-t.C:
            cancel()
        case <-read:
            if !t.Stop() {
                <-t.C
            }
            t.Reset(timeout)
        }
    }

Stop , , , Reset, , , , , .

+2

Source: https://habr.com/ru/post/1687634/


All Articles