Skip to content

Commit

Permalink
refactor(http): separate out the http client for better reuse
Browse files Browse the repository at this point in the history
  • Loading branch information
Arqu committed Jul 27, 2021
1 parent 0c37dd6 commit 95bebd2
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 302 deletions.
3 changes: 2 additions & 1 deletion cmd/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/qri-io/qri/auth/key"
"github.com/qri-io/qri/config"
"github.com/qri-io/qri/lib"
qhttp "github.com/qri-io/qri/lib/http"
"github.com/qri-io/qri/p2p"
)

Expand All @@ -22,7 +23,7 @@ type Factory interface {
CryptoGenerator() key.CryptoGenerator

Init() error
HTTPClient() *lib.HTTPClient
HTTPClient() *qhttp.Client
ConnectionNode() (*p2p.QriNode, error)
}

Expand Down
3 changes: 2 additions & 1 deletion cmd/qri.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/qri-io/qri/auth/key"
"github.com/qri-io/qri/config"
"github.com/qri-io/qri/lib"
qhttp "github.com/qri-io/qri/lib/http"
"github.com/qri-io/qri/p2p"
"github.com/qri-io/qri/remote"
"github.com/qri-io/qri/remote/access"
Expand Down Expand Up @@ -225,7 +226,7 @@ func (o *QriOptions) CryptoGenerator() key.CryptoGenerator {
}

// HTTPClient returns a client for performing RPC over HTTP
func (o *QriOptions) HTTPClient() *lib.HTTPClient {
func (o *QriOptions) HTTPClient() *qhttp.Client {
if err := o.Init(); err != nil {
return nil
}
Expand Down
4 changes: 3 additions & 1 deletion lib/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ var (
ErrDispatchNilInstance = errors.New("instance is nil, cannot dispatch")
// ErrDispatchNilParam indicates that the param passed to dispatch is nil
ErrDispatchNilParam = errors.New("param is nil, cannot dispatch")
// ErrUnsupportedRPC is an error for when running a method that is not supported via HTTP RPC
ErrUnsupportedRPC = errors.New("method is not supported over RPC")
)

// dispatcher isolates the dispatch method
Expand Down Expand Up @@ -130,7 +132,7 @@ func (inst *Instance) dispatchMethodCall(ctx context.Context, method string, par
// for it to reliably use GET. All POSTs w/ content type application json work, however.
// we may want to just flat out say that as an RPC layer, dispatch will only ever use
// json POST to communicate.
err = inst.http.CallMethod(ctx, c.Endpoint, "POST", source, param, res)
err = inst.http.CallMethod(ctx, c.Endpoint.String(), "POST", source, param, res)
if err != nil {
return nil, nil, err
}
Expand Down
202 changes: 2 additions & 200 deletions lib/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,214 +2,16 @@ package lib

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"

ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
apiutil "github.com/qri-io/qri/api/util"
"github.com/qri-io/qri/auth/token"
qhttp "github.com/qri-io/qri/lib/http"
)

// ErrUnsupportedRPC is an error for when running a method that is not supported via HTTP RPC
var ErrUnsupportedRPC = errors.New("method is not supported over RPC")

const jsonMimeType = "application/json"
const sourceResolver = "SourceResolver"

// HTTPClient implements the qri http client
type HTTPClient struct {
Address string
Protocol string
}

// NewHTTPClient instantiates a new HTTPClient
func NewHTTPClient(multiaddr string) (*HTTPClient, error) {
maAddr, err := ma.NewMultiaddr(multiaddr)
if err != nil {
return nil, err
}
// we default to the http protocol
protocol := "http"
protocols := maAddr.Protocols()
httpAddr, err := manet.ToNetAddr(maAddr)
if err != nil {
return nil, err
}
for _, p := range protocols {
// if https is present in the multiAddr we preffer that over http
if p.Code == ma.P_HTTPS {
protocol = "https"
}
}
return &HTTPClient{
Address: httpAddr.String(),
Protocol: protocol,
}, nil
}

// NewHTTPClientWithProtocol instantiates a new HTTPClient with a specified protocol
func NewHTTPClientWithProtocol(multiaddr string, protocol string) (*HTTPClient, error) {
maAddr, err := ma.NewMultiaddr(multiaddr)
if err != nil {
return nil, err
}
httpAddr, err := manet.ToNetAddr(maAddr)
if err != nil {
return nil, err
}
return &HTTPClient{
Address: httpAddr.String(),
Protocol: protocol,
}, nil
}

// Call calls API endpoint and passes on parameters, context info
func (c HTTPClient) Call(ctx context.Context, apiEndpoint APIEndpoint, source string, params interface{}, result interface{}) error {
return c.CallMethod(ctx, apiEndpoint, http.MethodPost, source, params, result)
}

// CallMethod calls API endpoint and passes on parameters, context info and specific HTTP Method
func (c HTTPClient) CallMethod(ctx context.Context, apiEndpoint APIEndpoint, httpMethod string, source string, params interface{}, result interface{}) error {
// TODO(arqu): work out mimeType configuration/override per API endpoint
mimeType := jsonMimeType
addr := fmt.Sprintf("%s://%s%s", c.Protocol, c.Address, apiEndpoint)

return c.do(ctx, addr, httpMethod, mimeType, source, params, result, false)
}

// CallRaw calls API endpoint and passes on parameters, context info and returns the []byte result
func (c HTTPClient) CallRaw(ctx context.Context, apiEndpoint APIEndpoint, source string, params interface{}, result interface{}) error {
return c.CallMethodRaw(ctx, apiEndpoint, http.MethodPost, source, params, result)
}

// CallMethodRaw calls API endpoint and passes on parameters, context info, specific HTTP Method and returns the []byte result
func (c HTTPClient) CallMethodRaw(ctx context.Context, apiEndpoint APIEndpoint, httpMethod string, source string, params interface{}, result interface{}) error {
// TODO(arqu): work out mimeType configuration/override per API endpoint
mimeType := jsonMimeType
addr := fmt.Sprintf("%s://%s%s", c.Protocol, c.Address, apiEndpoint)
// TODO(arqu): inject context values into headers

return c.do(ctx, addr, httpMethod, mimeType, source, params, result, true)
}

func (c HTTPClient) do(ctx context.Context, addr string, httpMethod string, mimeType string, source string, params interface{}, result interface{}, raw bool) error {
var req *http.Request
var err error

log.Debugf("http: %s - %s", httpMethod, addr)

if httpMethod == http.MethodGet || httpMethod == http.MethodDelete {
u, err := url.Parse(addr)
if err != nil {
return err
}

if params != nil {
if pm, ok := params.(map[string]string); ok {
qvars := u.Query()
for k, v := range pm {
qvars.Set(k, v)
}
u.RawQuery = qvars.Encode()
}
}
req, err = http.NewRequest(httpMethod, u.String(), nil)
} else if httpMethod == http.MethodPost || httpMethod == http.MethodPut {
payload, err := json.Marshal(params)
if err != nil {
return err
}
req, err = http.NewRequest(httpMethod, addr, bytes.NewReader(payload))
}
if err != nil {
return err
}

req.Header.Set("Content-Type", mimeType)
req.Header.Set("Accept", mimeType)

if source != "" {
req.Header.Set(sourceResolver, source)
}

req, added := token.AddContextTokenToRequest(ctx, req)
if !added {
log.Debugw("No token was set on an http client request. Unauthenticated requests may fail", "httpMethod", httpMethod, "addr", addr)
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}

if err = c.checkError(res, body, raw); err != nil {
return err
}

if raw {
if buf, ok := result.(*bytes.Buffer); ok {
buf.Write(body)
} else {
return fmt.Errorf("HTTPClient raw interface is not a byte buffer")
}
return nil
}

if result != nil {
resData := apiutil.Response{
Data: result,
Meta: &apiutil.Meta{},
}
err = json.Unmarshal(body, &resData)
if err != nil {
log.Debugf("HTTPClient response err: %s", err.Error())
return fmt.Errorf("HTTPClient response err: %s", err)
}
}
return nil
}

func (c HTTPClient) checkError(res *http.Response, body []byte, raw bool) error {
metaResponse := struct {
Meta *apiutil.Meta
}{
Meta: &apiutil.Meta{},
}
parseErr := json.Unmarshal(body, &metaResponse)

if !raw {
if parseErr != nil {
log.Debugf("HTTPClient response error: %d - %q", res.StatusCode, body)
return fmt.Errorf("failed parsing response: %q", string(body))
}
if metaResponse.Meta == nil {
log.Debugf("HTTPClient response error: %d - %q", res.StatusCode, body)
return fmt.Errorf("invalid meta response")
}
} else if (metaResponse.Meta.Code < 200 || metaResponse.Meta.Code > 299) && metaResponse.Meta.Code != 0 {
log.Debugf("HTTPClient response meta error: %d - %q", metaResponse.Meta.Code, metaResponse.Meta.Error)
return fmt.Errorf(metaResponse.Meta.Error)
}

if res.StatusCode < 200 || res.StatusCode > 299 {
log.Debugf("HTTPClient response error: %d - %q", res.StatusCode, body)
return fmt.Errorf(string(body))
}
return nil
}

// NewHTTPRequestHandler creates a JSON-API endpoint for a registered dispatch
// method
func NewHTTPRequestHandler(inst *Instance, libMethod string) http.HandlerFunc {
Expand Down Expand Up @@ -251,7 +53,7 @@ func NewHTTPRequestHandler(inst *Instance, libMethod string) http.HandlerFunc {

// SourceFromRequest retrieves from the http request the source for resolving refs
func SourceFromRequest(r *http.Request) string {
return r.Header.Get(sourceResolver)
return r.Header.Get(qhttp.SourceResolver)
}

// DecodeParams decodes a json body into params
Expand Down
Loading

0 comments on commit 95bebd2

Please sign in to comment.