mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +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
|
||||
|
||||
- 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.
|
||||
|
||||
- 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"
|
||||
"strconv"
|
||||
"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 {
|
||||
HTTPClient *http.Client
|
||||
|
||||
|
@ -25,12 +27,30 @@ type Client struct {
|
|||
|
||||
// A list of persistent request options.
|
||||
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 {
|
||||
c := &Client{
|
||||
HTTPClient: &http.Client{},
|
||||
PersistentRequestOptions: defaultRequestOptions,
|
||||
requestHandlers: defaultRequestHandlers,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
|
@ -40,6 +60,60 @@ func New(opts ...Option) *Client {
|
|||
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
|
||||
|
||||
// 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 adds or sets (if overridePrev is true) a header to the request.
|
||||
func RequestHeader(overridePrev bool, key string, values ...string) RequestOption {
|
||||
key = http.CanonicalHeaderKey(key)
|
||||
|
||||
|
@ -113,6 +188,12 @@ func (c *Client) Do(ctx context.Context, method, urlpath string, payload interfa
|
|||
ctx = context.Background()
|
||||
}
|
||||
|
||||
if c.rateLimiter != nil {
|
||||
if err := c.rateLimiter.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Method defaults to GET.
|
||||
if method == "" {
|
||||
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.
|
||||
// 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 (
|
||||
|
@ -189,11 +280,13 @@ const (
|
|||
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) {
|
||||
opts = append(opts, RequestHeader(true, contentTypeKey, contentTypeJSON))
|
||||
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) {
|
||||
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...)
|
||||
}
|
||||
|
||||
// Uploader holds the necessary information for upload requests.
|
||||
//
|
||||
// Look the Client.NewUploader method.
|
||||
type Uploader struct {
|
||||
client *Client
|
||||
|
||||
|
@ -212,6 +308,7 @@ type Uploader struct {
|
|||
Writer *multipart.Writer
|
||||
}
|
||||
|
||||
// AddFileSource adds a form field to the uploader with the given key.
|
||||
func (u *Uploader) AddField(key, value string) error {
|
||||
f, err := u.Writer.CreateFormField(key)
|
||||
if err != nil {
|
||||
|
@ -222,6 +319,7 @@ func (u *Uploader) AddField(key, value string) error {
|
|||
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 {
|
||||
f, err := u.Writer.CreateFormFile(key, filename)
|
||||
if err != nil {
|
||||
|
@ -232,6 +330,7 @@ func (u *Uploader) AddFileSource(key, filename string, source io.Reader) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// AddFile adds a local form file to the uploader with the given key.
|
||||
func (u *Uploader) AddFile(key, filename string) error {
|
||||
source, err := os.Open(filename)
|
||||
if err != nil {
|
||||
|
@ -241,6 +340,7 @@ func (u *Uploader) AddFile(key, filename string) error {
|
|||
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) {
|
||||
err := u.Writer.Close()
|
||||
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...)
|
||||
}
|
||||
|
||||
// NewUploader returns a structure which is responsible for sending
|
||||
// file and form data to the server.
|
||||
func (c *Client) NewUploader() *Uploader {
|
||||
body := new(bytes.Buffer)
|
||||
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 {
|
||||
if payload != nil {
|
||||
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,
|
||||
// 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 {
|
||||
apiErr, ok := GetError(err)
|
||||
if !ok {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package client
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// 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...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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