|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-kratos/kratos/v2/encoding"
|
|
|
|
xhttp "github.com/go-kratos/kratos/v2/internal/http"
|
|
|
|
"github.com/go-kratos/kratos/v2/middleware"
|
|
|
|
"github.com/go-kratos/kratos/v2/transport"
|
|
|
|
spb "google.golang.org/genproto/googleapis/rpc/status"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DecodeErrorFunc is decode error func.
|
|
|
|
type DecodeErrorFunc func(ctx context.Context, w *http.Response) error
|
|
|
|
|
|
|
|
// ClientOption is HTTP client option.
|
|
|
|
type ClientOption func(*clientOptions)
|
|
|
|
|
|
|
|
// WithTimeout with client request timeout.
|
|
|
|
func WithTimeout(d time.Duration) ClientOption {
|
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.timeout = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithUserAgent with client user agent.
|
|
|
|
func WithUserAgent(ua string) ClientOption {
|
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.userAgent = ua
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithTransport with client transport.
|
|
|
|
func WithTransport(trans http.RoundTripper) ClientOption {
|
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.transport = trans
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithMiddleware with client middleware.
|
|
|
|
func WithMiddleware(m middleware.Middleware) ClientOption {
|
|
|
|
return func(o *clientOptions) {
|
|
|
|
o.middleware = m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client is a HTTP transport client.
|
|
|
|
type clientOptions struct {
|
|
|
|
ctx context.Context
|
|
|
|
timeout time.Duration
|
|
|
|
userAgent string
|
|
|
|
transport http.RoundTripper
|
|
|
|
errorDecoder DecodeErrorFunc
|
|
|
|
middleware middleware.Middleware
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient returns an HTTP client.
|
|
|
|
func NewClient(ctx context.Context, opts ...ClientOption) (*http.Client, error) {
|
|
|
|
trans, err := NewTransport(ctx, opts...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &http.Client{Transport: trans}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTransport creates an http.RoundTripper.
|
|
|
|
func NewTransport(ctx context.Context, opts ...ClientOption) (http.RoundTripper, error) {
|
|
|
|
options := &clientOptions{
|
|
|
|
ctx: ctx,
|
|
|
|
timeout: 500 * time.Millisecond,
|
|
|
|
transport: http.DefaultTransport,
|
|
|
|
errorDecoder: checkResponse,
|
|
|
|
}
|
|
|
|
for _, o := range opts {
|
|
|
|
o(options)
|
|
|
|
}
|
|
|
|
return &baseTransport{
|
|
|
|
errorDecoder: options.errorDecoder,
|
|
|
|
middleware: options.middleware,
|
|
|
|
userAgent: options.userAgent,
|
|
|
|
timeout: options.timeout,
|
|
|
|
base: options.transport,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type baseTransport struct {
|
|
|
|
userAgent string
|
|
|
|
timeout time.Duration
|
|
|
|
base http.RoundTripper
|
|
|
|
errorDecoder DecodeErrorFunc
|
|
|
|
middleware middleware.Middleware
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *baseTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
if t.userAgent != "" && req.Header.Get("User-Agent") == "" {
|
|
|
|
req.Header.Set("User-Agent", t.userAgent)
|
|
|
|
}
|
|
|
|
ctx := transport.NewContext(req.Context(), transport.Transport{Kind: transport.KindHTTP})
|
|
|
|
ctx = NewClientContext(ctx, ClientInfo{Request: req})
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.timeout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
h := func(ctx context.Context, in interface{}) (interface{}, error) {
|
|
|
|
res, err := t.base.RoundTrip(in.(*http.Request))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := t.errorDecoder(ctx, res); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
if t.middleware != nil {
|
|
|
|
h = t.middleware(h)
|
|
|
|
}
|
|
|
|
res, err := h(ctx, req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return res.(*http.Response), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do send an HTTP request and decodes the body of response into target.
|
|
|
|
// returns an error (of type *Error) if the response status code is not 2xx.
|
|
|
|
func Do(client *http.Client, req *http.Request, target interface{}) error {
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
subtype := xhttp.ContentSubtype(res.Header.Get(xhttp.HeaderContentType))
|
|
|
|
codec := encoding.GetCodec(subtype)
|
|
|
|
if codec == nil {
|
|
|
|
codec = encoding.GetCodec("json")
|
|
|
|
}
|
|
|
|
data, err := ioutil.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return codec.Unmarshal(data, target)
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkResponse returns an error (of type *Error) if the response
|
|
|
|
// status code is not 2xx.
|
|
|
|
func checkResponse(ctx context.Context, res *http.Response) error {
|
|
|
|
if res.StatusCode >= 200 && res.StatusCode <= 299 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
if data, err := ioutil.ReadAll(res.Body); err == nil {
|
|
|
|
st := new(spb.Status)
|
|
|
|
if err = protojson.Unmarshal(data, st); err == nil {
|
|
|
|
return status.ErrorProto(st)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return status.Error(xhttp.GRPCCodeFromStatus(res.StatusCode), res.Status)
|
|
|
|
}
|