context: Add ctxhttp package.
ctxhttp provides helper functions for performing context-aware HTTP
requests.
Fixes golang/go#11904
Change-Id: Ib9d2fef48953dbb52f0a70e1ed49ea7fe12b7801
Reviewed-on: https://go-review.googlesource.com/12755
Reviewed-by: David Symonds <dsymonds@golang.org>
Reviewed-by: Aaron Jacobs <jacobsa@google.com>
Reviewed-by: Dave Day <djd@golang.org>
diff --git a/context/ctxhttp/cancelreq.go b/context/ctxhttp/cancelreq.go
new file mode 100644
index 0000000..48610e3
--- /dev/null
+++ b/context/ctxhttp/cancelreq.go
@@ -0,0 +1,18 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.5
+
+package ctxhttp
+
+import "net/http"
+
+func canceler(client *http.Client, req *http.Request) func() {
+ ch := make(chan struct{})
+ req.Cancel = ch
+
+ return func() {
+ close(ch)
+ }
+}
diff --git a/context/ctxhttp/cancelreq_go14.go b/context/ctxhttp/cancelreq_go14.go
new file mode 100644
index 0000000..56bcbad
--- /dev/null
+++ b/context/ctxhttp/cancelreq_go14.go
@@ -0,0 +1,23 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.5
+
+package ctxhttp
+
+import "net/http"
+
+type requestCanceler interface {
+ CancelRequest(*http.Request)
+}
+
+func canceler(client *http.Client, req *http.Request) func() {
+ rc, ok := client.Transport.(requestCanceler)
+ if !ok {
+ return func() {}
+ }
+ return func() {
+ rc.CancelRequest(req)
+ }
+}
diff --git a/context/ctxhttp/ctxhttp.go b/context/ctxhttp/ctxhttp.go
new file mode 100644
index 0000000..504dd63
--- /dev/null
+++ b/context/ctxhttp/ctxhttp.go
@@ -0,0 +1,79 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
+package ctxhttp // import "golang.org/x/net/context/ctxhttp"
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "golang.org/x/net/context"
+)
+
+// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
+// If the client is nil, http.DefaultClient is used.
+// If the context is canceled or times out, ctx.Err() will be returned.
+func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
+ if client == nil {
+ client = http.DefaultClient
+ }
+
+ // Request cancelation changed in Go 1.5, see cancelreq.go and cancelreq_go14.go.
+ cancel := canceler(client, req)
+
+ type responseAndError struct {
+ resp *http.Response
+ err error
+ }
+ result := make(chan responseAndError, 1)
+
+ go func() {
+ resp, err := client.Do(req)
+ result <- responseAndError{resp, err}
+ }()
+
+ select {
+ case <-ctx.Done():
+ cancel()
+ return nil, ctx.Err()
+ case r := <-result:
+ return r.resp, r.err
+ }
+}
+
+// Get issues a GET request via the Do function.
+func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ return Do(ctx, client, req)
+}
+
+// Head issues a HEAD request via the Do function.
+func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
+ req, err := http.NewRequest("HEAD", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ return Do(ctx, client, req)
+}
+
+// Post issues a POST request via the Do function.
+func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
+ req, err := http.NewRequest("POST", url, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", bodyType)
+ return Do(ctx, client, req)
+}
+
+// PostForm issues a POST request via the Do function.
+func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
+ return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+}
diff --git a/context/ctxhttp/ctxhttp_test.go b/context/ctxhttp/ctxhttp_test.go
new file mode 100644
index 0000000..47b53d7
--- /dev/null
+++ b/context/ctxhttp/ctxhttp_test.go
@@ -0,0 +1,72 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ctxhttp
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+)
+
+const (
+ requestDuration = 100 * time.Millisecond
+ requestBody = "ok"
+)
+
+func TestNoTimeout(t *testing.T) {
+ ctx := context.Background()
+ resp, err := doRequest(ctx)
+
+ if resp == nil || err != nil {
+ t.Fatalf("error received from client: %v %v", err, resp)
+ }
+}
+func TestCancel(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ go func() {
+ time.Sleep(requestDuration / 2)
+ cancel()
+ }()
+
+ resp, err := doRequest(ctx)
+
+ if resp != nil || err == nil {
+ t.Fatalf("expected error, didn't get one. resp: %v", resp)
+ }
+ if err != ctx.Err() {
+ t.Fatalf("expected error from context but got: %v", err)
+ }
+}
+
+func TestCancelAfterRequest(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ resp, err := doRequest(ctx)
+
+ // Cancel before reading the body.
+ // Request.Body should still be readable after the context is canceled.
+ cancel()
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil || string(b) != requestBody {
+ t.Fatalf("could not read body: %q %v", b, err)
+ }
+}
+
+func doRequest(ctx context.Context) (*http.Response, error) {
+ var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(requestDuration)
+ w.Write([]byte(requestBody))
+ })
+
+ serv := httptest.NewServer(okHandler)
+ defer serv.Close()
+
+ return Get(ctx, nil, serv.URL)
+}