Skip to content

Commit b0e403b

Browse files
committed
feat: support generic endpoints in http transport
Signed-off-by: Mark Sagi-Kazar <[email protected]>
1 parent d0b7cac commit b0e403b

File tree

7 files changed

+923
-0
lines changed

7 files changed

+923
-0
lines changed

transport/http2/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package http provides a general purpose HTTP binding for endpoints.
2+
package http

transport/http2/encode_decode.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
// DecodeRequestFunc extracts a user-domain request object from an HTTP
9+
// request object. It's designed to be used in HTTP servers, for server-side
10+
// endpoints. One straightforward DecodeRequestFunc could be something that
11+
// JSON decodes from the request body to the concrete request type.
12+
type DecodeRequestFunc[Req any] func(context.Context, *http.Request) (request Req, err error)
13+
14+
// EncodeRequestFunc encodes the passed request object into the HTTP request
15+
// object. It's designed to be used in HTTP clients, for client-side
16+
// endpoints. One straightforward EncodeRequestFunc could be something that JSON
17+
// encodes the object directly to the request body.
18+
type EncodeRequestFunc func(context.Context, *http.Request, interface{}) error
19+
20+
// CreateRequestFunc creates an outgoing HTTP request based on the passed
21+
// request object. It's designed to be used in HTTP clients, for client-side
22+
// endpoints. It's a more powerful version of EncodeRequestFunc, and can be used
23+
// if more fine-grained control of the HTTP request is required.
24+
type CreateRequestFunc func(context.Context, interface{}) (*http.Request, error)
25+
26+
// EncodeResponseFunc encodes the passed response object to the HTTP response
27+
// writer. It's designed to be used in HTTP servers, for server-side
28+
// endpoints. One straightforward EncodeResponseFunc could be something that
29+
// JSON encodes the object directly to the response body.
30+
type EncodeResponseFunc[Resp any] func(context.Context, http.ResponseWriter, Resp) error
31+
32+
// DecodeResponseFunc extracts a user-domain response object from an HTTP
33+
// response object. It's designed to be used in HTTP clients, for client-side
34+
// endpoints. One straightforward DecodeResponseFunc could be something that
35+
// JSON decodes from the response body to the concrete response type.
36+
type DecodeResponseFunc func(context.Context, *http.Response) (response interface{}, err error)

transport/http2/example_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
)
9+
10+
func ExamplePopulateRequestContext() {
11+
handler := NewServer[any, any](
12+
func(ctx context.Context, request interface{}) (response interface{}, err error) {
13+
fmt.Println("Method", ctx.Value(ContextKeyRequestMethod).(string))
14+
fmt.Println("RequestPath", ctx.Value(ContextKeyRequestPath).(string))
15+
fmt.Println("RequestURI", ctx.Value(ContextKeyRequestURI).(string))
16+
fmt.Println("X-Request-ID", ctx.Value(ContextKeyRequestXRequestID).(string))
17+
return struct{}{}, nil
18+
},
19+
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
20+
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
21+
ServerBefore[any, any](PopulateRequestContext),
22+
)
23+
24+
server := httptest.NewServer(handler)
25+
defer server.Close()
26+
27+
req, _ := http.NewRequest("PATCH", fmt.Sprintf("%s/search?q=sympatico", server.URL), nil)
28+
req.Header.Set("X-Request-Id", "a1b2c3d4e5")
29+
http.DefaultClient.Do(req)
30+
31+
// Output:
32+
// Method PATCH
33+
// RequestPath /search
34+
// RequestURI /search?q=sympatico
35+
// X-Request-ID a1b2c3d4e5
36+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
// RequestFunc may take information from an HTTP request and put it into a
9+
// request context. In Servers, RequestFuncs are executed prior to invoking the
10+
// endpoint. In Clients, RequestFuncs are executed after creating the request
11+
// but prior to invoking the HTTP client.
12+
type RequestFunc func(context.Context, *http.Request) context.Context
13+
14+
// ServerResponseFunc may take information from a request context and use it to
15+
// manipulate a ResponseWriter. ServerResponseFuncs are only executed in
16+
// servers, after invoking the endpoint but prior to writing a response.
17+
type ServerResponseFunc func(context.Context, http.ResponseWriter) context.Context
18+
19+
// ClientResponseFunc may take information from an HTTP request and make the
20+
// response available for consumption. ClientResponseFuncs are only executed in
21+
// clients, after a request has been made, but prior to it being decoded.
22+
type ClientResponseFunc func(context.Context, *http.Response) context.Context
23+
24+
// SetContentType returns a ServerResponseFunc that sets the Content-Type header
25+
// to the provided value.
26+
func SetContentType(contentType string) ServerResponseFunc {
27+
return SetResponseHeader("Content-Type", contentType)
28+
}
29+
30+
// SetResponseHeader returns a ServerResponseFunc that sets the given header.
31+
func SetResponseHeader(key, val string) ServerResponseFunc {
32+
return func(ctx context.Context, w http.ResponseWriter) context.Context {
33+
w.Header().Set(key, val)
34+
return ctx
35+
}
36+
}
37+
38+
// SetRequestHeader returns a RequestFunc that sets the given header.
39+
func SetRequestHeader(key, val string) RequestFunc {
40+
return func(ctx context.Context, r *http.Request) context.Context {
41+
r.Header.Set(key, val)
42+
return ctx
43+
}
44+
}
45+
46+
// PopulateRequestContext is a RequestFunc that populates several values into
47+
// the context from the HTTP request. Those values may be extracted using the
48+
// corresponding ContextKey type in this package.
49+
func PopulateRequestContext(ctx context.Context, r *http.Request) context.Context {
50+
for k, v := range map[contextKey]string{
51+
ContextKeyRequestMethod: r.Method,
52+
ContextKeyRequestURI: r.RequestURI,
53+
ContextKeyRequestPath: r.URL.Path,
54+
ContextKeyRequestProto: r.Proto,
55+
ContextKeyRequestHost: r.Host,
56+
ContextKeyRequestRemoteAddr: r.RemoteAddr,
57+
ContextKeyRequestXForwardedFor: r.Header.Get("X-Forwarded-For"),
58+
ContextKeyRequestXForwardedProto: r.Header.Get("X-Forwarded-Proto"),
59+
ContextKeyRequestAuthorization: r.Header.Get("Authorization"),
60+
ContextKeyRequestReferer: r.Header.Get("Referer"),
61+
ContextKeyRequestUserAgent: r.Header.Get("User-Agent"),
62+
ContextKeyRequestXRequestID: r.Header.Get("X-Request-Id"),
63+
ContextKeyRequestAccept: r.Header.Get("Accept"),
64+
} {
65+
ctx = context.WithValue(ctx, k, v)
66+
}
67+
return ctx
68+
}
69+
70+
type contextKey int
71+
72+
const (
73+
// ContextKeyRequestMethod is populated in the context by
74+
// PopulateRequestContext. Its value is r.Method.
75+
ContextKeyRequestMethod contextKey = iota
76+
77+
// ContextKeyRequestURI is populated in the context by
78+
// PopulateRequestContext. Its value is r.RequestURI.
79+
ContextKeyRequestURI
80+
81+
// ContextKeyRequestPath is populated in the context by
82+
// PopulateRequestContext. Its value is r.URL.Path.
83+
ContextKeyRequestPath
84+
85+
// ContextKeyRequestProto is populated in the context by
86+
// PopulateRequestContext. Its value is r.Proto.
87+
ContextKeyRequestProto
88+
89+
// ContextKeyRequestHost is populated in the context by
90+
// PopulateRequestContext. Its value is r.Host.
91+
ContextKeyRequestHost
92+
93+
// ContextKeyRequestRemoteAddr is populated in the context by
94+
// PopulateRequestContext. Its value is r.RemoteAddr.
95+
ContextKeyRequestRemoteAddr
96+
97+
// ContextKeyRequestXForwardedFor is populated in the context by
98+
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-For").
99+
ContextKeyRequestXForwardedFor
100+
101+
// ContextKeyRequestXForwardedProto is populated in the context by
102+
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-Proto").
103+
ContextKeyRequestXForwardedProto
104+
105+
// ContextKeyRequestAuthorization is populated in the context by
106+
// PopulateRequestContext. Its value is r.Header.Get("Authorization").
107+
ContextKeyRequestAuthorization
108+
109+
// ContextKeyRequestReferer is populated in the context by
110+
// PopulateRequestContext. Its value is r.Header.Get("Referer").
111+
ContextKeyRequestReferer
112+
113+
// ContextKeyRequestUserAgent is populated in the context by
114+
// PopulateRequestContext. Its value is r.Header.Get("User-Agent").
115+
ContextKeyRequestUserAgent
116+
117+
// ContextKeyRequestXRequestID is populated in the context by
118+
// PopulateRequestContext. Its value is r.Header.Get("X-Request-Id").
119+
ContextKeyRequestXRequestID
120+
121+
// ContextKeyRequestAccept is populated in the context by
122+
// PopulateRequestContext. Its value is r.Header.Get("Accept").
123+
ContextKeyRequestAccept
124+
125+
// ContextKeyResponseHeaders is populated in the context whenever a
126+
// ServerFinalizerFunc is specified. Its value is of type http.Header, and
127+
// is captured only once the entire response has been written.
128+
ContextKeyResponseHeaders
129+
130+
// ContextKeyResponseSize is populated in the context whenever a
131+
// ServerFinalizerFunc is specified. Its value is of type int64.
132+
ContextKeyResponseSize
133+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package http_test
2+
3+
import (
4+
"context"
5+
"net/http/httptest"
6+
"testing"
7+
8+
httptransport "github.com/go-kit/kit/transport/http2"
9+
)
10+
11+
func TestSetHeader(t *testing.T) {
12+
const (
13+
key = "X-Foo"
14+
val = "12345"
15+
)
16+
r := httptest.NewRecorder()
17+
httptransport.SetResponseHeader(key, val)(context.Background(), r)
18+
if want, have := val, r.Header().Get(key); want != have {
19+
t.Errorf("want %q, have %q", want, have)
20+
}
21+
}
22+
23+
func TestSetContentType(t *testing.T) {
24+
const contentType = "application/json"
25+
r := httptest.NewRecorder()
26+
httptransport.SetContentType(contentType)(context.Background(), r)
27+
if want, have := contentType, r.Header().Get("Content-Type"); want != have {
28+
t.Errorf("want %q, have %q", want, have)
29+
}
30+
}

0 commit comments

Comments
 (0)