如何提前关闭/中止 Golang 的 http.Client POST 请求

huangapple go评论78阅读模式
英文:

how to close/abort a Golang http.Client POST prematurely

问题

我正在使用http.Client来实现客户端的长轮询:

resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonPostBytes))
if err != nil {
	panic(err)
}
defer resp.Body.Close()

var results []*ResponseMessage
err = json.NewDecoder(resp.Body).Decode(&results)  // 在长轮询时,代码在这里阻塞

有没有一种标准的方法可以在客户端取消请求?

我想调用resp.Body.Close()可以实现取消请求,但是我必须从另一个goroutine中调用它,因为客户端通常已经在读取长轮询的响应时被阻塞。

我知道可以通过http.Transport设置超时,但是我的应用逻辑需要根据用户操作来取消请求,而不仅仅是超时。

英文:

I'm using http.Client for the client-side implementation of a long-poll:

resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonPostBytes))
if err != nil {
	panic(err)
}
defer resp.Body.Close()

var results []*ResponseMessage
err = json.NewDecoder(resp.Body).Decode(&results)  // code blocks here on long-poll

Is there a standard way to pre-empt/cancel the request from the client-side?

I imagine that calling resp.Body.Close() would do it, but I'd have to call that from another goroutine, as the client is normally already blocked in reading the response of the long-poll.

I know that there is a way to set a timeout via http.Transport, but my app logic need to do the cancellation based on a user action, not just a timeout.

答案1

得分: 27

使用CancelRequest现在已经被弃用。

当前的策略是使用http.Request.WithContext传递一个带有截止时间或将被取消的上下文。
之后就像正常的请求一样使用它。

req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
req = req.WithContext(ctx)
resp, err := client.Do(req)
// ...
英文:

Using CancelRequest is now deprecated.

The current strategy is to use http.Request.WithContext passing a context with a deadline or that will be canceled otherwise.
Just use it like a normal request afterwards.

req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
req = req.WithContext(ctx)
resp, err := client.Do(req)
// ...

答案2

得分: 19

标准的方法是使用类型为context.Context的上下文,并将其传递给所有需要知道请求何时取消的函数。

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // 在一个 goroutine 中运行 HTTP 请求,并将响应传递给 f 函数。
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // 等待 f 函数返回。
        return ctx.Err()
    case err := <-c:
        return err
    }
}

golang.org/x/net/context

// Context 携带截止时间、取消信号和请求范围的值,跨 API 边界传递。
// 它的方法可以被多个 goroutine 同时使用。
type Context interface {
    // Done 返回一个通道,当此 Context 被取消或超时时关闭。
    Done() <-chan struct{}

    // Err 在 Done 通道关闭后,指示此上下文被取消的原因。
    Err() error

    // Deadline 返回此 Context 将被取消的时间(如果有)。
    Deadline() (deadline time.Time, ok bool)

    // Value 返回与键关联的值,如果没有则返回 nil。
    Value(key interface{}) interface{}
}

更多信息请参考 https://blog.golang.org/context

更新

正如 Paulo 提到的,Request.Cancel 现在已被弃用,作者应该将上下文传递给请求本身(使用 *Request.WithContext),并使用上下文的取消通道(取消请求)。

package main

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

func main() {
	cx, cancel := context.WithCancel(context.Background())
	req, _ := http.NewRequest("GET", "http://google.com", nil)
	req = req.WithContext(cx)
	ch := make(chan error)

	go func() {
		_, err := http.DefaultClient.Do(req)
		select {
		case <-cx.Done():
			// 已超时
		default:
			ch <- err
		}
	}()

	// 模拟用户取消请求
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel()
	}()
	select {
	case err := <-ch:
		if err != nil {
			// HTTP 错误
			panic(err)
		}
		print("没有错误")
	case <-cx.Done():
		panic(cx.Err())
	}

}
英文:

The standard way is to use a context of type context.Context and pass it around to all the functions that need to know when the request is cancelled.

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &amp;http.Transport{}
    client := &amp;http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c &lt;- f(client.Do(req)) }()
    select {
    case &lt;-ctx.Done():
        tr.CancelRequest(req)
        &lt;-c // Wait for f to return.
        return ctx.Err()
    case err := &lt;-c:
        return err
    }
}

golang.org/x/net/context

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() &lt;-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Source and more on https://blog.golang.org/context

Update

As Paulo mentioned, Request.Cancel is now deprecated and the author should pass the context to the request itself(using *Request.WithContext) and use the cancellation channel of the context(to cancel the request).

package main

import (
	&quot;context&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

func main() {
	cx, cancel := context.WithCancel(context.Background())
	req, _ := http.NewRequest(&quot;GET&quot;, &quot;http://google.com&quot;, nil)
	req = req.WithContext(cx)
	ch := make(chan error)

	go func() {
		_, err := http.DefaultClient.Do(req)
		select {
		case &lt;-cx.Done():
			// Already timedout
		default:
			ch &lt;- err
		}
	}()

	// Simulating user cancel request
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel()
	}()
	select {
	case err := &lt;-ch:
		if err != nil {
			// HTTP error
			panic(err)
		}
		print(&quot;no error&quot;)
	case &lt;-cx.Done():
		panic(cx.Err())
	}

}

答案3

得分: 7

不,client.Post是一个方便的包装器,适用于90%的情况,不需要请求取消。

可能只需重新实现您的客户端以访问底层的Transport对象,该对象具有CancelRequest()函数。

这是一个快速的示例:

package main

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

func main() {
	req, _ := http.NewRequest("GET", "http://google.com", nil)
	tr := &http.Transport{} // TODO: copy defaults from http.DefaultTransport
	client := &http.Client{Transport: tr}
	c := make(chan error, 1)
	go func() {
		resp, err := client.Do(req)
		// 处理响应...
		_ = resp
		c <- err
	}()

	// 模拟用户取消请求的通道
	user := make(chan struct{}, 0)
	go func() {
		time.Sleep(100 * time.Millisecond)
		user <- struct{}{}
	}()

	for {
		select {
		case <-user:
			log.Println("取消请求")
			tr.CancelRequest(req)
		case err := <-c:
			log.Println("客户端完成:", err)
			return
		}
	}
}
英文:

Nope, client.Post is a handy wrapper for 90% of use-cases where request cancellation is not needed.

Probably it will be enough simply to reimplement your client to get access to underlying Transport object, which has CancelRequest() function.

Just a quick example:

package main

import (
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

func main() {
	req, _ := http.NewRequest(&quot;GET&quot;, &quot;http://google.com&quot;, nil)
	tr := &amp;http.Transport{} // TODO: copy defaults from http.DefaultTransport
	client := &amp;http.Client{Transport: tr}
	c := make(chan error, 1)
	go func() {
		resp, err := client.Do(req)
		// handle response ...
		_ = resp
		c &lt;- err
	}()

    // Simulating user cancel request channel
	user := make(chan struct{}, 0)
	go func() {
		time.Sleep(100 * time.Millisecond)
		user &lt;- struct{}{}
	}()

	for {
		select {
		case &lt;-user:
			log.Println(&quot;Cancelling request&quot;)
			tr.CancelRequest(req)
		case err := &lt;-c:
			log.Println(&quot;Client finished:&quot;, err)
			return
		}
	}
}

答案4

得分: 2

自Go 1.13版本开始,除了使用NewRequestRequest.WithContext之外,还可以使用NewRequestWithContext函数来为HTTP请求附加context.Context。这个函数接受一个控制创建的传出请求整个生命周期的Context,适用于Client.DoTransport.RoundTrip的使用。

以下是示例代码的转换:

req, err := http.NewRequest(...)
if err != nil {...}
req.WithContext(ctx)

转换为:

req, err := http.NewRequestWithContext(ctx, ...)
if err != nil {...}
英文:

To add to the other answers that attach context.Context to http requests, since 1.13 we have:

> A new function NewRequestWithContext has been added and it accepts a Context that controls the entire lifetime of the created outgoing Request, suitable for use with Client.Do and Transport.RoundTrip.

https://golang.org/doc/go1.13#net/http

This function can be used instead of using NewRequest and then Request.WithContext.

req, err := http.NewRequest(...)
if err != nil {...}
req.WithContext(ctx)

becomes

req, err := http.NewRequestWithContext(ctx, ...)
if err != nil {...}

</details>



# 答案5
**得分**: 1

@Paulo Casaretto的答案是正确的,应该使用[http.Request.WithContext](https://golang.org/pkg/net/http/#Request.WithContext)。

以下是一个完整的示例(请注意时间数字:5、10、30秒)。

HTTP服务器:

```go
package main

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

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("before sleep")
	time.Sleep(10 * time.Second)
	fmt.Println("after sleep")

	fmt.Fprintf(w, "Hi")
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":9191", nil))
}

HTTP服务器控制台输出:

before sleep
after sleep

HTTP客户端:

package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	go func() {
		fmt.Println("before request")
		client := &http.Client{Timeout: 30 * time.Second}
		req, err := http.NewRequest("GET", "http://127.0.0.1:9191", nil)
		if err != nil {
			panic(err)
		}
		req = req.WithContext(ctx)
		_, err = client.Do(req)
		if err != nil {
			panic(err)
		}
		fmt.Println("will not reach here")
	}()

	time.Sleep(5 * time.Second)
	cancel()
	fmt.Println("finished")
}

HTTP客户端控制台输出:

before request
finished
英文:

@Paulo Casaretto 's answer is right, should using
http.Request.WithContext.

Here is a full demo (be aware of the time numbers: 5, 10, 30 seconds).

HTTP Server:

package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Println(&quot;before sleep&quot;)
	time.Sleep(10 * time.Second)
	fmt.Println(&quot;after sleep&quot;)

	fmt.Fprintf(w, &quot;Hi&quot;)
}

func main() {
	http.HandleFunc(&quot;/&quot;, handler)
	log.Fatal(http.ListenAndServe(&quot;:9191&quot;, nil))
}

The HTTP Server console print:

before sleep
after sleep 

HTTP Client:

package main

import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	go func() {
		fmt.Println(&quot;before request&quot;)
		client := &amp;http.Client{Timeout: 30 * time.Second}
		req, err := http.NewRequest(&quot;GET&quot;, &quot;http://127.0.0.1:9191&quot;, nil)
		if err != nil {
			panic(err)
		}
		req = req.WithContext(ctx)
		_, err = client.Do(req)
		if err != nil {
			panic(err)
		}
		fmt.Println(&quot;will not reach here&quot;)
	}()

	time.Sleep(5 * time.Second)
	cancel()
	fmt.Println(&quot;finished&quot;)
}

The HTTP Client console print:

before request
finished

huangapple
  • 本文由 发表于 2015年3月23日 01:43:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/29197685.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定