Skip to content

Commit 6ffbf01

Browse files
authored
Merge pull request #81 from sideshow/feature-context
Feature request context
2 parents 6d33787 + 6e82204 commit 6ffbf01

File tree

8 files changed

+180
-7
lines changed

8 files changed

+180
-7
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ before_install:
1515

1616
install:
1717
- go get golang.org/x/net/http2
18+
- go get golang.org/x/net/context
1819
- go get golang.org/x/crypto/pkcs12
1920

2021
os:

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ if res.Sent() {
124124
}
125125
```
126126

127+
## Context & Timeouts
128+
129+
For better control over request cancelations and timeouts APNS/2 supports
130+
contexts. Using a context can be helpful if you want to cancel all pushes when
131+
the parent process is cancelled, or need finer grained control over individual
132+
push timeouts. See the [Google post](https://blog.golang.org/context) for more
133+
information on contexts.
134+
135+
```go
136+
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
137+
res, err := client.PushWithContext(ctx, notification)
138+
defer cancel()
139+
```
140+
127141
## Command line tool
128142

129143
APNS/2 has a command line tool that can be installed with `go get github.com/sideshow/apns2/apns2`. Usage:

client.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var (
3232
// HTTPClientTimeout specifies a time limit for requests made by the
3333
// HTTPClient. The timeout includes connection time, any redirects,
3434
// and reading the response body.
35-
HTTPClientTimeout = 30 * time.Second
35+
HTTPClientTimeout = 60 * time.Second
3636
)
3737

3838
// Client represents a connection with the APNs
@@ -93,17 +93,32 @@ func (c *Client) Production() *Client {
9393
// transparently before sending the notification. It will return a Response
9494
// indicating whether the notification was accepted or rejected by the APNs
9595
// gateway, or an error if something goes wrong.
96+
//
97+
// Use PushWithContext if you need better cancelation and timeout control.
9698
func (c *Client) Push(n *Notification) (*Response, error) {
97-
payload, err := json.Marshal(n)
99+
return c.PushWithContext(nil, n)
100+
}
98101

102+
// PushWithContext sends a Notification to the APNs gateway. Context carries a
103+
// deadline and a cancelation signal and allows you to close long running
104+
// requests when the context timeout is exceeded. Context can be nil, for
105+
// backwards compatibility.
106+
//
107+
// If the underlying http.Client is not currently connected, this method will
108+
// attempt to reconnect transparently before sending the notification. It will
109+
// return a Response indicating whether the notification was accepted or
110+
// rejected by the APNs gateway, or an error if something goes wrong.
111+
func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error) {
112+
payload, err := json.Marshal(n)
99113
if err != nil {
100114
return nil, err
101115
}
102116

103117
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
104118
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
105119
setHeaders(req, n)
106-
httpRes, err := c.HTTPClient.Do(req)
120+
121+
httpRes, err := c.requestWithContext(ctx, req)
107122
if err != nil {
108123
return nil, err
109124
}

client_go16.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// +build go1.6,!go1.7
2+
3+
package apns2
4+
5+
import (
6+
"net/http"
7+
8+
"golang.org/x/net/context"
9+
"golang.org/x/net/context/ctxhttp"
10+
)
11+
12+
// A Context carries a deadline, a cancelation signal, and other values across
13+
// API boundaries.
14+
//
15+
// Context's methods may be called by multiple goroutines simultaneously.
16+
type Context interface {
17+
context.Context
18+
}
19+
20+
func (c *Client) requestWithContext(ctx Context, req *http.Request) (*http.Response, error) {
21+
if ctx != nil {
22+
return ctxhttp.Do(ctx, c.HTTPClient, req)
23+
}
24+
return c.HTTPClient.Do(req)
25+
}

client_go16_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// +build go1.6
2+
// +build !go1.7
3+
4+
package apns2_test
5+
6+
import (
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
"golang.org/x/net/context"
14+
)
15+
16+
func TestClientPushWithContextWithTimeout(t *testing.T) {
17+
const timeout = time.Nanosecond
18+
n := mockNotification()
19+
var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6"
20+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
22+
w.Header().Set("apns-id", apnsID)
23+
w.WriteHeader(http.StatusOK)
24+
}))
25+
defer server.Close()
26+
27+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
28+
time.Sleep(timeout)
29+
res, err := mockClient(server.URL).PushWithContext(ctx, n)
30+
assert.Error(t, err)
31+
assert.Nil(t, res)
32+
cancel()
33+
}
34+
35+
func TestClientPushWithContext(t *testing.T) {
36+
n := mockNotification()
37+
var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6"
38+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
40+
w.Header().Set("apns-id", apnsID)
41+
w.WriteHeader(http.StatusOK)
42+
}))
43+
defer server.Close()
44+
45+
res, err := mockClient(server.URL).PushWithContext(context.Background(), n)
46+
assert.Nil(t, err)
47+
assert.Equal(t, res.ApnsID, apnsID)
48+
}

client_go17.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// +build go1.7
2+
3+
package apns2
4+
5+
import (
6+
"context"
7+
"net/http"
8+
)
9+
10+
// A Context carries a deadline, a cancelation signal, and other values across
11+
// API boundaries.
12+
//
13+
// Context's methods may be called by multiple goroutines simultaneously.
14+
type Context interface {
15+
context.Context
16+
}
17+
18+
func (c *Client) requestWithContext(ctx Context, req *http.Request) (*http.Response, error) {
19+
if ctx != nil {
20+
req = req.WithContext(ctx)
21+
}
22+
return c.HTTPClient.Do(req)
23+
}

client_go17_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// +build go1.7
2+
3+
package apns2_test
4+
5+
import (
6+
"context"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestClientPushWithContextWithTimeout(t *testing.T) {
16+
const timeout = time.Nanosecond
17+
n := mockNotification()
18+
var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6"
19+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
21+
w.Header().Set("apns-id", apnsID)
22+
w.WriteHeader(http.StatusOK)
23+
}))
24+
defer server.Close()
25+
26+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
27+
time.Sleep(timeout)
28+
res, err := mockClient(server.URL).PushWithContext(ctx, n)
29+
assert.Error(t, err)
30+
assert.Nil(t, res)
31+
cancel()
32+
}
33+
34+
func TestClientPushWithContext(t *testing.T) {
35+
n := mockNotification()
36+
var apnsID = "02ABC856-EF8D-4E49-8F15-7B8A61D978D6"
37+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
39+
w.Header().Set("apns-id", apnsID)
40+
w.WriteHeader(http.StatusOK)
41+
}))
42+
defer server.Close()
43+
44+
res, err := mockClient(server.URL).PushWithContext(context.Background(), n)
45+
assert.Nil(t, err)
46+
assert.Equal(t, res.ApnsID, apnsID)
47+
}

client_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ func TestClientBadTransportError(t *testing.T) {
6969
}
7070

7171
func TestClientNameToCertificate(t *testing.T) {
72-
certificate, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid.p12", "")
73-
client := apns.NewClient(certificate)
72+
crt, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid.p12", "")
73+
client := apns.NewClient(crt)
7474
name := client.HTTPClient.Transport.(*http2.Transport).TLSClientConfig.NameToCertificate
7575
assert.Len(t, name, 1)
7676

@@ -82,8 +82,8 @@ func TestClientNameToCertificate(t *testing.T) {
8282

8383
func TestDialTLSTimeout(t *testing.T) {
8484
apns.TLSDialTimeout = 1 * time.Millisecond
85-
certificate, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid.p12", "")
86-
client := apns.NewClient(certificate)
85+
crt, _ := certificate.FromP12File("certificate/_fixtures/certificate-valid.p12", "")
86+
client := apns.NewClient(crt)
8787
dialTLS := client.HTTPClient.Transport.(*http2.Transport).DialTLS
8888
listener, err := net.Listen("tcp", "127.0.0.1:0")
8989
if err != nil {

0 commit comments

Comments
 (0)