Compare commits

...

23 Commits

Author SHA1 Message Date
ee9b7493e3 fix repeated detection
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-19 10:08:56 +03:00
Renovate Bot
0182d6ab56 Update module github.com/unistack-org/micro/v3 to v3.3.14 2021-04-19 04:33:26 +00:00
d99e97090c support repeated url values
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-19 02:10:24 +03:00
8de7912a91 detect response content-type
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-18 15:45:42 +03:00
6ccb40bab0 minor content-type fixes
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-16 17:09:29 +03:00
2b16a8a7a6 allow to set content type with charset
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-10 12:46:34 +03:00
Renovate Bot
d051256839 Update module github.com/unistack-org/micro/v3 to v3.3.13 2021-04-10 00:54:11 +00:00
68b32989fc allow to override content-type
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-09 23:12:25 +03:00
21a41a8e03 fix message body parsing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-09 22:55:01 +03:00
9150958044 fix x-www-form-urlencoded requests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-03 11:55:22 +03:00
Renovate Bot
e358db44ca Update module github.com/unistack-org/micro/v3 to v3.3.10 2021-04-01 00:36:29 +00:00
5cdd48329e metadata.FromContext => metadata.FromOutgoingContext
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-31 10:43:38 +03:00
Renovate Bot
01b5e1db54 Update module github.com/unistack-org/micro/v3 to v3.3.9 2021-03-30 01:17:00 +00:00
Renovate Bot
900e08458b Update module github.com/unistack-org/micro/v3 to v3.3.8 2021-03-29 01:30:57 +00:00
Renovate Bot
f4fff1c77a Update module github.com/unistack-org/micro/v3 to v3.3.4 2021-03-26 16:34:13 +00:00
4bbf97a309 add minor dev comment
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-26 15:52:52 +03:00
02f29b0ef3 fix tls issues
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-26 15:48:57 +03:00
Renovate Bot
ba69a7dfcd Update module github.com/unistack-org/micro/v3 to v3.3.2 2021-03-25 14:38:19 +00:00
Renovate Bot
45aee3c441 Update module github.com/unistack-org/micro/v3 to v3.3.1 2021-03-24 23:34:28 +00:00
1787f44c54 cleanup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-24 15:49:20 +03:00
1bbdf03f60 add aditional check for nil message passed
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-24 10:44:53 +03:00
Renovate Bot
2bb1299e56 Update module github.com/unistack-org/micro/v3 to v3.2.26 2021-03-23 17:35:57 +00:00
fd1acd56cb add tag support
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-23 00:29:27 +03:00
9 changed files with 355 additions and 124 deletions

2
go.mod
View File

@@ -2,4 +2,4 @@ module github.com/unistack-org/micro-client-http/v3
go 1.16 go 1.16
require github.com/unistack-org/micro/v3 v3.2.24 require github.com/unistack-org/micro/v3 v3.3.14

10
go.sum
View File

@@ -5,13 +5,13 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I= github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
github.com/unistack-org/micro/v3 v3.2.24 h1:3sQ72wy8Vap9KDD7bdLOn+dhlPT0iXSJRXhs2qHoKxA= github.com/unistack-org/micro/v3 v3.3.14 h1:CAkDMjHZT8/D6GGF5h3gK84m6tlWZC17IGPb2GkAn/4=
github.com/unistack-org/micro/v3 v3.2.24/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8= github.com/unistack-org/micro/v3 v3.3.14/go.mod h1:ETGcQQUcjxGaD44LUMX+0fgo8Loh7ExldfIPLvfUmDo=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

174
http.go
View File

@@ -11,6 +11,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings"
"sync"
"time" "time"
"github.com/unistack-org/micro/v3/broker" "github.com/unistack-org/micro/v3/broker"
@@ -34,11 +36,16 @@ type httpClient struct {
opts client.Options opts client.Options
dialer *net.Dialer dialer *net.Dialer
httpcli *http.Client httpcli *http.Client
init bool
sync.RWMutex
} }
func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) { func newRequest(addr string, req client.Request, ct string, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
hreq := &http.Request{Method: http.MethodPost} hreq := &http.Request{Method: http.MethodPost}
body := "*" // as like google api http annotation body := "*" // as like google api http annotation
var tags []string
var scheme string
u, err := url.Parse(addr) u, err := url.Parse(addr)
if err != nil { if err != nil {
hreq.URL = &url.URL{ hreq.URL = &url.URL{
@@ -47,6 +54,7 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
Path: req.Endpoint(), Path: req.Endpoint(),
} }
hreq.Host = addr hreq.Host = addr
scheme = "http"
} else { } else {
ep := req.Endpoint() ep := req.Endpoint()
if opts.Context != nil { if opts.Context != nil {
@@ -59,6 +67,10 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
if b, ok := opts.Context.Value(bodyKey{}).(string); ok { if b, ok := opts.Context.Value(bodyKey{}).(string); ok {
body = b body = b
} }
if t, ok := opts.Context.Value(structTagsKey{}).([]string); ok && len(t) > 0 {
tags = t
}
} }
hreq.URL, err = u.Parse(ep) hreq.URL, err = u.Parse(ep)
if err != nil { if err != nil {
@@ -66,52 +78,65 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
} }
} }
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg) if len(tags) == 0 {
switch ct {
default:
tags = append(tags, "json", "protobuf")
case "text/xml":
tags = append(tags, "xml")
}
}
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg, tags)
if err != nil { if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error()) return nil, errors.BadRequest("go.micro.client", err.Error())
} }
if scheme != "" {
hreq.URL, err = url.Parse(scheme + "://" + addr + path)
} else {
hreq.URL, err = url.Parse(addr + path) hreq.URL, err = url.Parse(addr + path)
}
if err != nil { if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error()) return nil, errors.BadRequest("go.micro.client", err.Error())
} }
var b []byte b, err := cf.Marshal(nmsg)
// marshal request is struct not empty
if nmsg != nil {
b, err = cf.Marshal(nmsg)
if err != nil { if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error()) return nil, errors.BadRequest("go.micro.client", err.Error())
} }
}
if len(b) > 0 {
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b)) hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
hreq.ContentLength = int64(len(b)) hreq.ContentLength = int64(len(b))
}
return hreq, nil return hreq, nil
} }
func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error { func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
header := make(http.Header, 2) header := make(http.Header, 2)
if md, ok := metadata.FromContext(ctx); ok { if md, ok := metadata.FromOutgoingContext(ctx); ok {
for k, v := range md { for k, v := range md {
header.Set(k, v) header.Set(k, v)
} }
} }
ct := req.ContentType()
if len(opts.ContentType) > 0 {
ct = opts.ContentType
}
// set timeout in nanoseconds // set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout)) header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request // set the content type for the request
header.Set("Content-Type", req.ContentType()) header.Set("Content-Type", ct)
// get codec cf, err := h.newCodec(ct)
cf, err := h.newCodec(req.ContentType())
if err != nil { if err != nil {
return errors.InternalServerError("go.micro.client", err.Error()) return errors.InternalServerError("go.micro.client", err.Error())
} }
hreq, err := newRequest(addr, req, ct, cf, req.Body(), opts)
hreq, err := newRequest(addr, req, cf, req.Body(), opts)
if err != nil { if err != nil {
return err return err
} }
@@ -136,13 +161,19 @@ func (h *httpClient) call(ctx context.Context, addr string, req client.Request,
defer hrsp.Body.Close() defer hrsp.Body.Close()
return parseRsp(ctx, hrsp, cf, rsp, opts) if ct == "application/x-www-form-urlencoded" {
cf, err = h.newCodec(DefaultContentType)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
}
return h.parseRsp(ctx, hrsp, rsp, opts)
} }
func (h *httpClient) stream(ctx context.Context, addr string, req client.Request, opts client.CallOptions) (client.Stream, error) { func (h *httpClient) stream(ctx context.Context, addr string, req client.Request, opts client.CallOptions) (client.Stream, error) {
var header http.Header var header http.Header
if md, ok := metadata.FromContext(ctx); ok { if md, ok := metadata.FromOutgoingContext(ctx); ok {
header = make(http.Header, len(md)+2) header = make(http.Header, len(md)+2)
for k, v := range md { for k, v := range md {
header.Set(k, v) header.Set(k, v)
@@ -151,13 +182,18 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
header = make(http.Header, 2) header = make(http.Header, 2)
} }
ct := req.ContentType()
if len(opts.ContentType) > 0 {
ct = opts.ContentType
}
// set timeout in nanoseconds // set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout)) header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request // set the content type for the request
header.Set("Content-Type", req.ContentType()) header.Set("Content-Type", ct)
// get codec // get codec
cf, err := h.newCodec(req.ContentType()) cf, err := h.newCodec(ct)
if err != nil { if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error()) return nil, errors.InternalServerError("go.micro.client", err.Error())
} }
@@ -167,7 +203,7 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
if err == nil && u.Scheme != "" && u.Host != "" { if err == nil && u.Scheme != "" && u.Host != "" {
dialAddr = u.Host dialAddr = u.Host
} }
cc, err := h.dialer.DialContext(ctx, "tcp", addr) cc, err := (h.httpcli.Transport).(*http.Transport).DialContext(ctx, "tcp", addr)
if err != nil { if err != nil {
return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error dialing: %v", err)) return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error dialing: %v", err))
} }
@@ -178,7 +214,8 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
closed: make(chan bool), closed: make(chan bool),
opts: opts, opts: opts,
conn: cc, conn: cc,
codec: cf, ct: ct,
cf: cf,
header: header, header: header,
reader: bufio.NewReader(cc), reader: bufio.NewReader(cc),
request: req, request: req,
@@ -186,6 +223,13 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
} }
func (h *httpClient) newCodec(ct string) (codec.Codec, error) { func (h *httpClient) newCodec(ct string) (codec.Codec, error) {
h.RLock()
defer h.RUnlock()
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
if c, ok := h.opts.Codecs[ct]; ok { if c, ok := h.opts.Codecs[ct]; ok {
return c, nil return c, nil
} }
@@ -194,9 +238,32 @@ func (h *httpClient) newCodec(ct string) (codec.Codec, error) {
} }
func (h *httpClient) Init(opts ...client.Option) error { func (h *httpClient) Init(opts ...client.Option) error {
if len(opts) == 0 && h.init {
return nil
}
for _, o := range opts { for _, o := range opts {
o(&h.opts) o(&h.opts)
} }
if err := h.opts.Broker.Init(); err != nil {
return err
}
if err := h.opts.Tracer.Init(); err != nil {
return err
}
if err := h.opts.Router.Init(); err != nil {
return err
}
if err := h.opts.Logger.Init(); err != nil {
return err
}
if err := h.opts.Meter.Init(); err != nil {
return err
}
if err := h.opts.Transport.Init(); err != nil {
return err
}
return nil return nil
} }
@@ -208,8 +275,8 @@ func (h *httpClient) NewMessage(topic string, msg interface{}, opts ...client.Me
return newHTTPMessage(topic, msg, h.opts.ContentType, opts...) return newHTTPMessage(topic, msg, h.opts.ContentType, opts...)
} }
func (h *httpClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request { func (h *httpClient) NewRequest(service, method string, req interface{}, opts ...client.RequestOption) client.Request {
return newHTTPRequest(service, method, req, h.opts.ContentType, reqOpts...) return newHTTPRequest(service, method, req, h.opts.ContentType, opts...)
} }
func (h *httpClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { func (h *httpClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
@@ -478,7 +545,7 @@ func (h *httpClient) Stream(ctx context.Context, req client.Request, opts ...cli
func (h *httpClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error { func (h *httpClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
options := client.NewPublishOptions(opts...) options := client.NewPublishOptions(opts...)
md, ok := metadata.FromContext(ctx) md, ok := metadata.FromOutgoingContext(ctx)
if !ok { if !ok {
md = metadata.New(2) md = metadata.New(2)
} }
@@ -540,17 +607,31 @@ func NewClient(opts ...client.Option) client.Client {
opts: options, opts: options,
} }
dialer, ok := options.Context.Value(httpDialerKey{}).(*net.Dialer)
if !ok {
dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
}
if httpcli, ok := options.Context.Value(httpClientKey{}).(*http.Client); ok { if httpcli, ok := options.Context.Value(httpClientKey{}).(*http.Client); ok {
rc.httpcli = httpcli rc.httpcli = httpcli
} else { } else {
rc.httpcli = http.DefaultClient // TODO customTransport := http.DefaultTransport.(*http.Transport).Clone()
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxConnsPerHost: 100,
MaxIdleConns: 20,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: options.TLSConfig,
} }
if dialer, ok := options.Context.Value(httpDialerKey{}).(*net.Dialer); ok { rc.httpcli = &http.Client{Transport: tr}
rc.dialer = dialer
} else {
rc.dialer = &net.Dialer{}
} }
c := client.Client(rc) c := client.Client(rc)
// wrap in reverse // wrap in reverse
@@ -560,38 +641,3 @@ func NewClient(opts ...client.Option) client.Client {
return c return c
} }
func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
b, err := ioutil.ReadAll(hrsp.Body)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
if hrsp.StatusCode < 400 {
// unmarshal
if err := cf.Unmarshal(b, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if !ok || errmap == nil {
// user not provide map of errors
// id: req.Service() ??
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
if !ok {
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if cerr := cf.Unmarshal(b, err); cerr != nil {
return errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}

View File

@@ -15,7 +15,7 @@ type Request struct {
func TestValidPath(t *testing.T) { func TestValidPath(t *testing.T) {
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10} req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req) p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -32,7 +32,7 @@ func TestValidPath(t *testing.T) {
func TestInvalidPath(t *testing.T) { func TestInvalidPath(t *testing.T) {
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10} req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req) p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req, nil)
if err == nil { if err == nil {
t.Fatalf("path param must not be filled") t.Fatalf("path param must not be filled")
} }

View File

@@ -1,7 +1,6 @@
package http package http
import ( import (
"crypto/tls"
"net" "net"
"net/http" "net/http"
@@ -33,28 +32,21 @@ type tlsAuth struct{}
type maxRecvMsgSizeKey struct{} type maxRecvMsgSizeKey struct{}
type maxSendMsgSizeKey struct{} type maxSendMsgSizeKey struct{}
// maximum streams on a connectioin // PoolMaxStreams maximum streams on a connectioin
func PoolMaxStreams(n int) client.Option { func PoolMaxStreams(n int) client.Option {
return client.SetOption(poolMaxStreams{}, n) return client.SetOption(poolMaxStreams{}, n)
} }
// maximum idle conns of a pool // PoolMaxIdle maximum idle conns of a pool
func PoolMaxIdle(d int) client.Option { func PoolMaxIdle(d int) client.Option {
return client.SetOption(poolMaxIdle{}, d) return client.SetOption(poolMaxIdle{}, d)
} }
// AuthTLS should be used to setup a secure authentication using TLS
func AuthTLS(t *tls.Config) client.Option {
return client.SetOption(tlsAuth{}, t)
}
//
// MaxRecvMsgSize set the maximum size of message that client can receive. // MaxRecvMsgSize set the maximum size of message that client can receive.
func MaxRecvMsgSize(s int) client.Option { func MaxRecvMsgSize(s int) client.Option {
return client.SetOption(maxRecvMsgSizeKey{}, s) return client.SetOption(maxRecvMsgSizeKey{}, s)
} }
//
// MaxSendMsgSize set the maximum size of message that client can send. // MaxSendMsgSize set the maximum size of message that client can send.
func MaxSendMsgSize(s int) client.Option { func MaxSendMsgSize(s int) client.Option {
return client.SetOption(maxSendMsgSizeKey{}, s) return client.SetOption(maxSendMsgSizeKey{}, s)
@@ -95,3 +87,9 @@ type errorMapKey struct{}
func ErrorMap(m map[string]interface{}) client.CallOption { func ErrorMap(m map[string]interface{}) client.CallOption {
return client.SetCallOption(errorMapKey{}, m) return client.SetCallOption(errorMapKey{}, m)
} }
type structTagsKey struct{}
func StructTags(tags []string) client.CallOption {
return client.SetCallOption(structTagsKey{}, tags)
}

View File

@@ -15,7 +15,6 @@ type httpRequest struct {
func newHTTPRequest(service, method string, request interface{}, contentType string, opts ...client.RequestOption) client.Request { func newHTTPRequest(service, method string, request interface{}, contentType string, opts ...client.RequestOption) client.Request {
options := client.NewRequestOptions(opts...) options := client.NewRequestOptions(opts...)
if len(options.ContentType) == 0 { if len(options.ContentType) == 0 {
options.ContentType = contentType options.ContentType = contentType
} }

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"context" "context"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"sync" "sync"
@@ -18,7 +19,8 @@ type httpStream struct {
sync.RWMutex sync.RWMutex
address string address string
opts client.CallOptions opts client.CallOptions
codec codec.Codec ct string
cf codec.Codec
context context.Context context context.Context
header http.Header header http.Header
seq uint64 seq uint64
@@ -63,7 +65,7 @@ func (h *httpStream) Send(msg interface{}) error {
return errShutdown return errShutdown
} }
hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts) hreq, err := newRequest(h.address, h.request, h.ct, h.cf, msg, h.opts)
if err != nil { if err != nil {
return err return err
} }
@@ -88,7 +90,7 @@ func (h *httpStream) Recv(msg interface{}) error {
} }
defer hrsp.Body.Close() defer hrsp.Body.Close()
return parseRsp(h.context, hrsp, h.codec, msg, h.opts) return h.parseRsp(h.context, hrsp, h.cf, msg, h.opts)
} }
func (h *httpStream) Error() error { func (h *httpStream) Error() error {
@@ -106,3 +108,39 @@ func (h *httpStream) Close() error {
return h.conn.Close() return h.conn.Close()
} }
} }
func (h *httpStream) parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
var err error
// fast path return
if hrsp.StatusCode == http.StatusNoContent {
return nil
}
if hrsp.StatusCode < 400 {
if err = cf.ReadBody(hrsp.Body, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if ok && errmap != nil {
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
}
if err == nil {
buf, err := io.ReadAll(hrsp.Body)
if err != nil {
errors.InternalServerError("go.micro.client", err.Error())
}
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
}
if cerr := cf.ReadBody(hrsp.Body, err); cerr != nil {
err = errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}

139
util.go
View File

@@ -1,12 +1,17 @@
package http package http
import ( import (
"context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url"
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/errors"
rutil "github.com/unistack-org/micro/v3/util/reflect" rutil "github.com/unistack-org/micro/v3/util/reflect"
util "github.com/unistack-org/micro/v3/util/router" util "github.com/unistack-org/micro/v3/util/router"
) )
@@ -16,13 +21,18 @@ var (
mu sync.RWMutex mu sync.RWMutex
) )
func newPathRequest(path string, method string, body string, msg interface{}) (string, interface{}, error) { func newPathRequest(path string, method string, body string, msg interface{}, tags []string) (string, interface{}, error) {
// parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition // parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition
tpl, err := newTemplate(path) tpl, err := newTemplate(path)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if len(tpl.Fields) > 0 && msg == nil {
return "", nil, fmt.Errorf("nil message but path params requested: %v", path)
}
fieldsmapskip := make(map[string]struct{})
fieldsmap := make(map[string]string, len(tpl.Fields)) fieldsmap := make(map[string]string, len(tpl.Fields))
for _, v := range tpl.Fields { for _, v := range tpl.Fields {
fieldsmap[v] = "" fieldsmap[v] = ""
@@ -44,7 +54,7 @@ func newPathRequest(path string, method string, body string, msg interface{}) (s
tnmsg = tnmsg.Elem() tnmsg = tnmsg.Elem()
} }
values := make(map[string]string) values := url.Values{}
// copy cycle // copy cycle
for i := 0; i < tmsg.NumField(); i++ { for i := 0; i < tmsg.NumField(); i++ {
val := tmsg.Field(i) val := tmsg.Field(i)
@@ -52,20 +62,67 @@ func newPathRequest(path string, method string, body string, msg interface{}) (s
continue continue
} }
fld := tmsg.Type().Field(i) fld := tmsg.Type().Field(i)
lfield := strings.ToLower(fld.Name)
if _, ok := fieldsmap[lfield]; ok { t := &tag{}
fieldsmap[lfield] = fmt.Sprintf("%v", val.Interface()) for _, tn := range tags {
} else if (body == "*" || body == lfield) && method != http.MethodGet { ts, ok := fld.Tag.Lookup(tn)
if !ok {
continue
}
tp := strings.Split(ts, ",")
// special
switch tn {
case "protobuf": // special
for _, p := range tp {
if idx := strings.Index(p, "name="); idx > 0 {
t = &tag{key: tn, name: p[idx:]}
}
}
default:
t = &tag{key: tn, name: tp[0]}
}
if t.name != "" {
break
}
}
if t.name == "" {
// fallback to lowercase
t.name = strings.ToLower(fld.Name)
}
if !val.IsValid() || val.IsZero() {
continue
}
if _, ok := fieldsmap[t.name]; ok {
if val.Type().Kind() == reflect.Slice {
for idx := 0; idx < val.Len(); idx++ {
values.Add(t.name, fmt.Sprintf("%v", val.Index(idx).Interface()))
}
fieldsmapskip[t.name] = struct{}{}
} else {
fieldsmap[t.name] = fmt.Sprintf("%v", val.Interface())
}
} else if (body == "*" || body == t.name) && method != http.MethodGet {
tnmsg.Field(i).Set(val) tnmsg.Field(i).Set(val)
} else { } else {
values[lfield] = fmt.Sprintf("%v", val.Interface()) if val.Type().Kind() == reflect.Slice {
for idx := 0; idx < val.Len(); idx++ {
values.Add(t.name, fmt.Sprintf("%v", val.Index(idx).Interface()))
}
} else {
values.Add(t.name, fmt.Sprintf("%v", val.Interface()))
}
} }
} }
// check not filled stuff // check not filled stuff
for k, v := range fieldsmap { for k, v := range fieldsmap {
if v == "" { _, ok := fieldsmapskip[k]
return "", nil, fmt.Errorf("path param %s not filled %s", k, v) if !ok && v == "" {
return "", nil, fmt.Errorf("path param %s not filled", k)
} }
} }
@@ -73,23 +130,17 @@ func newPathRequest(path string, method string, body string, msg interface{}) (s
for _, fld := range tpl.Pool { for _, fld := range tpl.Pool {
_, _ = b.WriteRune('/') _, _ = b.WriteRune('/')
if v, ok := fieldsmap[fld]; ok { if v, ok := fieldsmap[fld]; ok {
if v != "" {
_, _ = b.WriteString(v) _, _ = b.WriteString(v)
}
} else { } else {
_, _ = b.WriteString(fld) _, _ = b.WriteString(fld)
} }
} }
idx := 0 if len(values) > 0 {
for k, v := range values {
if idx == 0 {
_, _ = b.WriteRune('?') _, _ = b.WriteRune('?')
} else { _, _ = b.WriteString(values.Encode())
_, _ = b.WriteRune('&')
}
_, _ = b.WriteString(k)
_, _ = b.WriteRune('=')
_, _ = b.WriteString(v)
idx++
} }
if rutil.IsZero(nmsg) { if rutil.IsZero(nmsg) {
@@ -120,3 +171,53 @@ func newTemplate(path string) (util.Template, error) {
return tpl, nil return tpl, nil
} }
func (h *httpClient) parseRsp(ctx context.Context, hrsp *http.Response, rsp interface{}, opts client.CallOptions) error {
// fast path return
if hrsp.StatusCode == http.StatusNoContent {
return nil
}
ct := DefaultContentType
if htype := hrsp.Header.Get("Content-Type"); htype != "" {
ct = htype
}
cf, err := h.newCodec(ct)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
if hrsp.StatusCode < 400 {
if err := cf.ReadBody(hrsp.Body, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if ok && errmap != nil {
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
}
if err == nil {
buf, err := io.ReadAll(hrsp.Body)
if err != nil {
errors.InternalServerError("go.micro.client", err.Error())
}
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
}
if cerr := cf.ReadBody(hrsp.Body, err); cerr != nil {
err = errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}
type tag struct {
key string
name string
}

View File

@@ -5,10 +5,19 @@ import (
"testing" "testing"
) )
func TestTemplate(t *testing.T) {
tpl, err := newTemplate("/v1/{ClientID}/list")
if err != nil {
t.Fatal(err)
}
_ = tpl
// fmt.Printf("%#+v\n", tpl.Pool)
}
func TestNewPathRequest(t *testing.T) { func TestNewPathRequest(t *testing.T) {
type Message struct { type Message struct {
Name string Name string `json:"name"`
Val1 string Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
Val2 int64 Val2 int64
Val3 []string Val3 []string
} }
@@ -16,7 +25,8 @@ func TestNewPathRequest(t *testing.T) {
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}} omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} { for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
path, nmsg, err := newPathRequest("/v1/test", m, "", omsg) body := ""
path, nmsg, err := newPathRequest("/v1/test", m, body, omsg, []string{"protobuf", "json"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -26,7 +36,46 @@ func TestNewPathRequest(t *testing.T) {
} }
vals := u.Query() vals := u.Query()
if v, ok := vals["name"]; !ok || v[0] != "test_name" { if v, ok := vals["name"]; !ok || v[0] != "test_name" {
t.Fatalf("invlid path: %v nmsg: %v", path, nmsg) t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
}
}
}
func TestNewPathVarRequest(t *testing.T) {
type Message struct {
Name string `json:"name"`
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
Val2 int64
Val3 []string
}
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
body := ""
if m != "GET" {
body = "*"
}
path, nmsg, err := newPathRequest("/v1/test/{val1}", m, body, omsg, []string{"protobuf", "json"})
if err != nil {
t.Fatal(err)
}
u, err := url.Parse(path)
if err != nil {
t.Fatal(err)
}
if m != "GET" {
if _, ok := nmsg.(*Message); !ok {
t.Fatalf("invalid nmsg: %#+v\n", nmsg)
}
if nmsg.(*Message).Name != "test_name" {
t.Fatalf("invalid nmsg: %v nmsg: %v", path, nmsg)
}
} else {
vals := u.Query()
if v, ok := vals["val2"]; !ok || v[0] != "100" {
t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
}
} }
} }
} }