Skip to content

Commit d9d99f4

Browse files
author
Michal Witkowski
committed
support Grpc-Timeout header for passing to the backend
1 parent 17b6bbc commit d9d99f4

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ More examples are available under `examples` directory.
198198
* Mapping streaming APIs to JSON streams
199199
* Mapping HTTP headers with `Grpc-Metadata-` prefix to gRPC metadata
200200
* Optionally emitting API definition for [Swagger](http://swagger.io).
201+
* Setting [gRPC timeouts](http://www.grpc.io/docs/guides/wire.html) through inbound HTTP `Grpc-Timeout` header.
201202
202203
### Want to support
203204
But not yet.

runtime/context.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package runtime
22

33
import (
4+
"fmt"
5+
"net"
46
"net/http"
7+
"strconv"
58
"strings"
6-
7-
"net"
9+
"time"
810

911
"golang.org/x/net/context"
1012
"google.golang.org/grpc/metadata"
1113
)
1214

1315
const metadataHeaderPrefix = "Grpc-Metadata-"
1416
const metadataTrailerPrefix = "Grpc-Trailer-"
17+
const metadataGrpcTimeout = "Grpc-Timeout"
18+
1519
const xForwardedFor = "X-Forwarded-For"
1620
const xForwardedHost = "X-Forwarded-Host"
1721

22+
var (
23+
// DefaultContextTimeout is used for gRPC call context.WithTimeout whenever a Grpc-Timeout inbound
24+
// header isn't present. If the value is 0 the sent `context` will not have a timeout.
25+
DefaultContextTimeout = 0 * time.Second
26+
)
27+
1828
/*
1929
AnnotateContext adds context information such as metadata from the request.
2030
@@ -23,6 +33,10 @@ will be the same context.
2333
*/
2434
func AnnotateContext(ctx context.Context, req *http.Request) context.Context {
2535
var pairs []string
36+
timeout := DefaultContextTimeout
37+
if tm := req.Header.Get(metadataGrpcTimeout); tm != "" {
38+
timeout, _ = timeoutDecode(tm)
39+
}
2640
for key, vals := range req.Header {
2741
for _, val := range vals {
2842
if key == "Authorization" {
@@ -47,7 +61,9 @@ func AnnotateContext(ctx context.Context, req *http.Request) context.Context {
4761
pairs = append(pairs, strings.ToLower(xForwardedFor), req.Header.Get(xForwardedFor)+", "+remoteIp)
4862
}
4963
}
50-
64+
if timeout != 0 {
65+
ctx, _ = context.WithTimeout(ctx, timeout)
66+
}
5167
if len(pairs) == 0 {
5268
return ctx
5369
}
@@ -72,3 +88,38 @@ func ServerMetadataFromContext(ctx context.Context) (md ServerMetadata, ok bool)
7288
md, ok = ctx.Value(serverMetadataKey{}).(ServerMetadata)
7389
return
7490
}
91+
92+
func timeoutDecode(s string) (time.Duration, error) {
93+
size := len(s)
94+
if size < 2 {
95+
return 0, fmt.Errorf("timeout string is too short: %q", s)
96+
}
97+
d, ok := timeoutUnitToDuration(s[size-1])
98+
if !ok {
99+
return 0, fmt.Errorf("timeout unit is not recognized: %q", s)
100+
}
101+
t, err := strconv.ParseInt(s[:size-1], 10, 64)
102+
if err != nil {
103+
return 0, err
104+
}
105+
return d * time.Duration(t), nil
106+
}
107+
108+
func timeoutUnitToDuration(u uint8) (d time.Duration, ok bool) {
109+
switch u {
110+
case 'H':
111+
return time.Hour, true
112+
case 'M':
113+
return time.Minute, true
114+
case 'S':
115+
return time.Second, true
116+
case 'm':
117+
return time.Millisecond, true
118+
case 'u':
119+
return time.Microsecond, true
120+
case 'n':
121+
return time.Nanosecond, true
122+
default:
123+
}
124+
return
125+
}

runtime/context_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55
"reflect"
66
"testing"
7+
"time"
78

89
"github.com/gengo/grpc-gateway/runtime"
910
"golang.org/x/net/context"
@@ -83,3 +84,22 @@ func TestAnnotateContext_XForwardedFor(t *testing.T) {
8384
t.Errorf("md[\"x-forwarded-for\"] = %v want %v", got, want)
8485
}
8586
}
87+
88+
func TestAnnotateContext_SupportsTimeouts(t *testing.T) {
89+
ctx := context.Background()
90+
request, _ := http.NewRequest("GET", "http://bar.foo.example.com", nil)
91+
annotated := runtime.AnnotateContext(ctx, request)
92+
if _, exists := annotated.Deadline(); exists {
93+
t.Errorf("by default Timeouts should not be set")
94+
}
95+
runtime.DefaultContextTimeout = 10 * time.Second
96+
annotated = runtime.AnnotateContext(ctx, request)
97+
if _, exists := annotated.Deadline(); !exists {
98+
t.Errorf("if DefaultContextTimeout is set, the annotated context should have a timeout")
99+
}
100+
request.Header.Add("Grpc-Timeout", "50S")
101+
annotated = runtime.AnnotateContext(ctx, request)
102+
if deadline, _ := annotated.Deadline(); deadline.Sub(time.Now()) < 2*time.Second { // in case time.Now() drifts
103+
t.Errorf("if Grpc-Timeout=50s is present, the timeout should be 50s")
104+
}
105+
}

0 commit comments

Comments
 (0)