mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
improvements to the x/client package
This commit is contained in:
parent
f633ab4b99
commit
4a1f0b6e9e
|
@ -28,6 +28,9 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
|
|
||||||
## Fixes and Improvements
|
## Fixes and Improvements
|
||||||
|
|
||||||
|
- New `RegisterRequestHandler` package-level and client methods to the new `x/client` package. Control or log the request-response lifecycle.
|
||||||
|
- New `RateLimit` HTTP Client option to the new `x/client` package.
|
||||||
|
|
||||||
- Push a security fix reported by [Kirill Efimov](https://github.com/kirill89) for older go runtimes.
|
- Push a security fix reported by [Kirill Efimov](https://github.com/kirill89) for older go runtimes.
|
||||||
|
|
||||||
- New `Configuration.Timeout` and `Configuration.TimeoutMessage` fields. Use it to set HTTP timeouts. Note that your http server's (`Application.ConfigureHost`) Read/Write timeouts should be a bit higher than the `Configuration.Timeout` in order to give some time to http timeout handler to kick in and be able to send the `Configuration.TimeoutMessage` properly.
|
- New `Configuration.Timeout` and `Configuration.TimeoutMessage` fields. Use it to set HTTP timeouts. Note that your http server's (`Application.ConfigureHost`) Read/Write timeouts should be a bit higher than the `Configuration.Timeout` in order to give some time to http timeout handler to kick in and be able to send the `Configuration.TimeoutMessage` properly.
|
||||||
|
|
|
@ -14,9 +14,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the base client
|
// A Client is an HTTP client. Initialize with the New package-level function.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
@ -25,12 +27,30 @@ type Client struct {
|
||||||
|
|
||||||
// A list of persistent request options.
|
// A list of persistent request options.
|
||||||
PersistentRequestOptions []RequestOption
|
PersistentRequestOptions []RequestOption
|
||||||
|
|
||||||
|
// Optional rate limiter instance initialized by the RateLimit method.
|
||||||
|
rateLimiter *rate.Limiter
|
||||||
|
|
||||||
|
// Optional handlers that are being fired before and after each new request.
|
||||||
|
requestHandlers []RequestHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New returns a new Iris HTTP Client.
|
||||||
|
// Available options:
|
||||||
|
// - BaseURL
|
||||||
|
// - Timeout
|
||||||
|
// - PersistentRequestOptions
|
||||||
|
// - RateLimit
|
||||||
|
//
|
||||||
|
// Look the Client.Do/JSON/... methods to send requests and
|
||||||
|
// ReadXXX methods to read responses.
|
||||||
|
//
|
||||||
|
// The default content type to send and receive data is JSON.
|
||||||
func New(opts ...Option) *Client {
|
func New(opts ...Option) *Client {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
HTTPClient: &http.Client{},
|
HTTPClient: &http.Client{},
|
||||||
PersistentRequestOptions: defaultRequestOptions,
|
PersistentRequestOptions: defaultRequestOptions,
|
||||||
|
requestHandlers: defaultRequestHandlers,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -40,6 +60,60 @@ func New(opts ...Option) *Client {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterRequestHandler registers one or more request handlers
|
||||||
|
// to be ran before and after of each new request.
|
||||||
|
//
|
||||||
|
// Request handler's BeginRequest method run after each request constructed
|
||||||
|
// and right before sent to the server.
|
||||||
|
//
|
||||||
|
// Request handler's EndRequest method run after response each received
|
||||||
|
// and right before methods return back to the caller.
|
||||||
|
//
|
||||||
|
// Any request handlers MUST be set right after the Client's initialization.
|
||||||
|
func (c *Client) RegisterRequestHandler(reqHandlers ...RequestHandler) {
|
||||||
|
reqHandlersToRegister := make([]RequestHandler, 0, len(reqHandlers))
|
||||||
|
for _, h := range reqHandlers {
|
||||||
|
if h == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reqHandlersToRegister = append(reqHandlersToRegister, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.requestHandlers = append(c.requestHandlers, reqHandlersToRegister...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) emitBeginRequest(ctx context.Context, req *http.Request) error {
|
||||||
|
if len(c.requestHandlers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range c.requestHandlers {
|
||||||
|
if hErr := h.BeginRequest(ctx, req); hErr != nil {
|
||||||
|
return hErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) emitEndRequest(ctx context.Context, resp *http.Response, err error) error {
|
||||||
|
if len(c.requestHandlers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range c.requestHandlers {
|
||||||
|
if hErr := h.EndRequest(ctx, resp, err); hErr != nil {
|
||||||
|
return hErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestOption declares the type of option one can pass
|
||||||
|
// to the Do methods(JSON, Form, ReadJSON...).
|
||||||
|
// Request options run before request constructed.
|
||||||
type RequestOption func(*http.Request) error
|
type RequestOption func(*http.Request) error
|
||||||
|
|
||||||
// We always add the following request headers, unless they're removed by custom ones.
|
// We always add the following request headers, unless they're removed by custom ones.
|
||||||
|
@ -47,6 +121,7 @@ var defaultRequestOptions = []RequestOption{
|
||||||
RequestHeader(false, acceptKey, contentTypeJSON),
|
RequestHeader(false, acceptKey, contentTypeJSON),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestHeader adds or sets (if overridePrev is true) a header to the request.
|
||||||
func RequestHeader(overridePrev bool, key string, values ...string) RequestOption {
|
func RequestHeader(overridePrev bool, key string, values ...string) RequestOption {
|
||||||
key = http.CanonicalHeaderKey(key)
|
key = http.CanonicalHeaderKey(key)
|
||||||
|
|
||||||
|
@ -113,6 +188,12 @@ func (c *Client) Do(ctx context.Context, method, urlpath string, payload interfa
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.rateLimiter != nil {
|
||||||
|
if err := c.rateLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Method defaults to GET.
|
// Method defaults to GET.
|
||||||
if method == "" {
|
if method == "" {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
|
@ -175,9 +256,19 @@ func (c *Client) Do(ctx context.Context, method, urlpath string, payload interfa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = c.emitBeginRequest(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Caller is responsible for closing the response body.
|
// Caller is responsible for closing the response body.
|
||||||
// Also note that the gzip compression is handled automatically nowadays.
|
// Also note that the gzip compression is handled automatically nowadays.
|
||||||
return c.HTTPClient.Do(req)
|
resp, respErr := c.HTTPClient.Do(req)
|
||||||
|
|
||||||
|
if err = c.emitEndRequest(ctx, resp, respErr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, respErr
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -189,11 +280,13 @@ const (
|
||||||
contentTypeFormURLEncoded = "application/x-www-form-urlencoded"
|
contentTypeFormURLEncoded = "application/x-www-form-urlencoded"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// JSON writes data as JSON to the server.
|
||||||
func (c *Client) JSON(ctx context.Context, method, urlpath string, payload interface{}, opts ...RequestOption) (*http.Response, error) {
|
func (c *Client) JSON(ctx context.Context, method, urlpath string, payload interface{}, opts ...RequestOption) (*http.Response, error) {
|
||||||
opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON))
|
opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON))
|
||||||
return c.Do(ctx, method, urlpath, payload, opts...)
|
return c.Do(ctx, method, urlpath, payload, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON writes form data to the server.
|
||||||
func (c *Client) Form(ctx context.Context, method, urlpath string, formValues url.Values, opts ...RequestOption) (*http.Response, error) {
|
func (c *Client) Form(ctx context.Context, method, urlpath string, formValues url.Values, opts ...RequestOption) (*http.Response, error) {
|
||||||
payload := formValues.Encode()
|
payload := formValues.Encode()
|
||||||
|
|
||||||
|
@ -205,6 +298,9 @@ func (c *Client) Form(ctx context.Context, method, urlpath string, formValues ur
|
||||||
return c.Do(ctx, method, urlpath, payload, opts...)
|
return c.Do(ctx, method, urlpath, payload, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uploader holds the necessary information for upload requests.
|
||||||
|
//
|
||||||
|
// Look the Client.NewUploader method.
|
||||||
type Uploader struct {
|
type Uploader struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
|
||||||
|
@ -212,6 +308,7 @@ type Uploader struct {
|
||||||
Writer *multipart.Writer
|
Writer *multipart.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddFileSource adds a form field to the uploader with the given key.
|
||||||
func (u *Uploader) AddField(key, value string) error {
|
func (u *Uploader) AddField(key, value string) error {
|
||||||
f, err := u.Writer.CreateFormField(key)
|
f, err := u.Writer.CreateFormField(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -222,6 +319,7 @@ func (u *Uploader) AddField(key, value string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddFileSource adds a form file to the uploader with the given key.
|
||||||
func (u *Uploader) AddFileSource(key, filename string, source io.Reader) error {
|
func (u *Uploader) AddFileSource(key, filename string, source io.Reader) error {
|
||||||
f, err := u.Writer.CreateFormFile(key, filename)
|
f, err := u.Writer.CreateFormFile(key, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -232,6 +330,7 @@ func (u *Uploader) AddFileSource(key, filename string, source io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddFile adds a local form file to the uploader with the given key.
|
||||||
func (u *Uploader) AddFile(key, filename string) error {
|
func (u *Uploader) AddFile(key, filename string) error {
|
||||||
source, err := os.Open(filename)
|
source, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -241,6 +340,7 @@ func (u *Uploader) AddFile(key, filename string) error {
|
||||||
return u.AddFileSource(key, filename, source)
|
return u.AddFileSource(key, filename, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uploads sends local data to the server.
|
||||||
func (u *Uploader) Upload(ctx context.Context, method, urlpath string, opts ...RequestOption) (*http.Response, error) {
|
func (u *Uploader) Upload(ctx context.Context, method, urlpath string, opts ...RequestOption) (*http.Response, error) {
|
||||||
err := u.Writer.Close()
|
err := u.Writer.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -253,6 +353,8 @@ func (u *Uploader) Upload(ctx context.Context, method, urlpath string, opts ...R
|
||||||
return u.client.Do(ctx, method, urlpath, payload, opts...)
|
return u.client.Do(ctx, method, urlpath, payload, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUploader returns a structure which is responsible for sending
|
||||||
|
// file and form data to the server.
|
||||||
func (c *Client) NewUploader() *Uploader {
|
func (c *Client) NewUploader() *Uploader {
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
|
@ -264,6 +366,8 @@ func (c *Client) NewUploader() *Uploader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadJSON binds "dest" to the response's body.
|
||||||
|
// After this call, the response body reader is closed.
|
||||||
func (c *Client) ReadJSON(ctx context.Context, dest interface{}, method, urlpath string, payload interface{}, opts ...RequestOption) error {
|
func (c *Client) ReadJSON(ctx context.Context, dest interface{}, method, urlpath string, payload interface{}, opts ...RequestOption) error {
|
||||||
if payload != nil {
|
if payload != nil {
|
||||||
opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON))
|
opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON))
|
||||||
|
|
|
@ -73,7 +73,8 @@ func DecodeError(err error, destPtr interface{}) error {
|
||||||
|
|
||||||
// GetErrorCode reads an error, which should be a type of APIError,
|
// GetErrorCode reads an error, which should be a type of APIError,
|
||||||
// and returns its status code.
|
// and returns its status code.
|
||||||
// If the given "err" is nil or is not an APIError it returns 200, acting as we have no error.
|
// If the given "err" is nil or is not an APIError it returns 200,
|
||||||
|
// acting as we have no error.
|
||||||
func GetErrorCode(err error) int {
|
func GetErrorCode(err error) int {
|
||||||
apiErr, ok := GetError(err)
|
apiErr, ok := GetError(err)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
// All the builtin client options should live here, for easy discovery.
|
// All the builtin client options should live here, for easy discovery.
|
||||||
|
|
||||||
|
@ -33,3 +37,12 @@ func PersistentRequestOptions(reqOpts ...RequestOption) Option {
|
||||||
c.PersistentRequestOptions = append(c.PersistentRequestOptions, reqOpts...)
|
c.PersistentRequestOptions = append(c.PersistentRequestOptions, reqOpts...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RateLimit configures the rate limit for requests.
|
||||||
|
//
|
||||||
|
// Defaults to zero which disables rate limiting.
|
||||||
|
func RateLimit(requestsPerSecond int) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.rateLimiter = rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
x/client/request_handler.go
Normal file
32
x/client/request_handler.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestHandler can be set to each Client instance and it should be
|
||||||
|
// responsible to handle the begin and end states of each request.
|
||||||
|
// Its BeginRequest fires right before the client talks to the server
|
||||||
|
// and its EndRequest fires right after the client receives a response from the server.
|
||||||
|
// If one of them return a non-nil error then the execution of client will stop and return that error.
|
||||||
|
type RequestHandler interface {
|
||||||
|
BeginRequest(context.Context, *http.Request) error
|
||||||
|
EndRequest(context.Context, *http.Response, error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultRequestHandlers []RequestHandler
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRequestHandler registers one or more request handlers
|
||||||
|
// to be ran before and after of each request on all newly created Iris HTTP Clients.
|
||||||
|
// Useful for Iris HTTP Client 3rd-party libraries
|
||||||
|
// e.g. on init register a custom request-response lifecycle logging.
|
||||||
|
func RegisterRequestHandler(reqHandlers ...RequestHandler) {
|
||||||
|
mu.Lock()
|
||||||
|
defaultRequestHandlers = append(defaultRequestHandlers, reqHandlers...)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user