| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- // Copyright 2019 Yunion
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package httputils
- import (
- "bytes"
- "compress/flate"
- "compress/gzip"
- "context"
- "crypto/tls"
- "fmt"
- "io"
- "io/ioutil"
- "net"
- "net/http"
- "net/http/httputil"
- "net/url"
- "os"
- "strconv"
- "strings"
- "syscall"
- "time"
- "github.com/fatih/color"
- "moul.io/http2curl/v2"
- "yunion.io/x/jsonutils"
- "yunion.io/x/pkg/appctx"
- "yunion.io/x/pkg/errors"
- "yunion.io/x/pkg/gotypes"
- "yunion.io/x/pkg/trace"
- "yunion.io/x/pkg/utils"
- )
- type THttpMethod string
- const (
- USER_AGENT = "yunioncloud-go/201708"
- GET = THttpMethod("GET")
- HEAD = THttpMethod("HEAD")
- POST = THttpMethod("POST")
- PUT = THttpMethod("PUT")
- PATCH = THttpMethod("PATCH")
- DELETE = THttpMethod("DELETE")
- OPTION = THttpMethod("OPTION")
- ConnectionTimeoutSeconds = 120
- IdleConnTimeoutSeconds = 60
- TLSHandshakeTimeoutSeconds = 10
- ResponseHeaderTimeoutSeconds = 30
- )
- var (
- red = color.New(color.FgRed, color.Bold).PrintlnFunc()
- green = color.New(color.FgGreen, color.Bold).PrintlnFunc()
- yellow = color.New(color.FgYellow, color.Bold).PrintlnFunc()
- cyan = color.New(color.FgHiCyan, color.Bold).PrintlnFunc()
- )
- type Error struct {
- Id string `json:"id,omitempty"`
- Fields []interface{} `json:"fields,omitempty"`
- }
- type JSONClientError struct {
- Request struct {
- Method string `json:"method,omitempty"`
- Url string `json:"url,omitempty"`
- Body jsonutils.JSONObject `json:"body,omitempty"`
- Headers map[string]string `json:"headers,omitempty"`
- } `json:"request,omitempty"`
- Code int `json:"code,omitzero"`
- Class string `json:"class,omitempty"`
- Details string `json:"details,omitempty"`
- Data Error `json:"data,omitempty"`
- }
- type sClient interface {
- Do(req *http.Request) (*http.Response, error)
- }
- // body might have been consumed, so body is provided separately
- func newJsonClientErrorFromRequest(req *http.Request, body string) *JSONClientError {
- return newJsonClientErrorFromRequest2(req.Method, req.URL.String(), req.Header, body)
- }
- func newJsonClientErrorFromRequest2(method string, urlStr string, hdrs http.Header, body string) *JSONClientError {
- jce := &JSONClientError{}
- jce.Request.Method = strings.ToUpper(method)
- jce.Request.Url = urlStr
- jce.Request.Headers = make(map[string]string)
- excludeHdrs := []string{
- "Accept",
- "Accept-Encoding",
- }
- authHdrs := []string{
- http.CanonicalHeaderKey("authorization"),
- http.CanonicalHeaderKey("x-auth-token"),
- http.CanonicalHeaderKey("x-subject-token"),
- }
- const (
- MAX_BODY = 128
- FIRST_PART = 100
- )
- switch jce.Request.Method {
- case "PUT", "POST", "PATCH":
- contType := hdrs.Get(http.CanonicalHeaderKey("content-type"))
- if len(body) > MAX_BODY {
- jce.Request.Body = jsonutils.NewString(body[:FIRST_PART] + "..." + body[len(body)-MAX_BODY+FIRST_PART+3:])
- } else if strings.Contains(contType, "json") {
- jce.Request.Body, _ = jsonutils.ParseString(body)
- } else if strings.Contains(contType, "xml") ||
- strings.Contains(contType, "x-www-form-urlencoded") {
- jce.Request.Body = jsonutils.NewString(body)
- }
- default:
- excludeHdrs = append(excludeHdrs, http.CanonicalHeaderKey("content-type"), http.CanonicalHeaderKey("content-length"))
- }
- for h := range hdrs {
- ch := http.CanonicalHeaderKey(h)
- if utils.IsInStringArray(ch, excludeHdrs) {
- continue
- }
- if utils.IsInStringArray(ch, authHdrs) {
- jce.Request.Headers[ch] = "*"
- } else {
- jce.Request.Headers[ch] = hdrs.Get(ch)
- }
- }
- return jce
- }
- type JSONClientErrorMsg struct {
- Error *JSONClientError
- }
- type JsonClient struct {
- client sClient
- }
- type JsonRequest interface {
- GetHttpMethod() THttpMethod
- GetRequestBody() jsonutils.JSONObject
- GetUrl() string
- SetHttpMethod(method THttpMethod)
- GetHeader() http.Header
- SetHeader(header http.Header)
- }
- type JsonBaseRequest struct {
- httpMethod THttpMethod
- url string
- params interface{}
- header http.Header
- }
- func (req *JsonBaseRequest) GetHttpMethod() THttpMethod {
- return req.httpMethod
- }
- func (req *JsonBaseRequest) GetRequestBody() jsonutils.JSONObject {
- if !gotypes.IsNil(req.params) {
- return jsonutils.Marshal(req.params)
- }
- return nil
- }
- func (req *JsonBaseRequest) GetUrl() string {
- return req.url
- }
- func (req *JsonBaseRequest) SetHttpMethod(method THttpMethod) {
- req.httpMethod = method
- }
- func (req *JsonBaseRequest) GetHeader() http.Header {
- return req.header
- }
- func (req *JsonBaseRequest) SetHeader(header http.Header) {
- for k, values := range header {
- req.header.Del(k)
- for _, v := range values {
- req.header.Add(k, v)
- }
- }
- }
- func NewJsonRequest(method THttpMethod, url string, params interface{}) *JsonBaseRequest {
- return &JsonBaseRequest{
- httpMethod: method,
- url: url,
- params: params,
- header: http.Header{"Content-Type": []string{"application/json"}},
- }
- }
- type JsonResponse interface {
- ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error
- }
- func (ce *JSONClientError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
- body.Unmarshal(ce)
- if ce.Code == 0 {
- ce.Code = statusCode
- }
- if len(ce.Class) == 0 {
- ce.Class = http.StatusText(statusCode)
- }
- if len(ce.Details) == 0 {
- ce.Details = body.String()
- }
- return ce
- }
- func NewJsonClient(client sClient) *JsonClient {
- return &JsonClient{client: client}
- }
- func (e *JSONClientError) Error() string {
- if !gotypes.IsNil(e.Request.Body) {
- if body, ok := e.Request.Body.(*jsonutils.JSONDict); ok && body.Contains("password") {
- body.Set("password", jsonutils.NewString("***"))
- e.Request.Body = body
- }
- }
- errMsg := JSONClientErrorMsg{Error: e}
- return jsonutils.Marshal(errMsg).String()
- }
- func (err *JSONClientError) Cause() error {
- if len(err.Class) > 0 {
- return errors.Error(err.Class)
- } else if err.Code >= 500 {
- return errors.ErrServer
- } else if err.Code >= 400 {
- return errors.ErrClient
- } else {
- return errors.ErrUnclassified
- }
- }
- func ErrorCode(err error) int {
- if err == nil {
- return 0
- }
- switch je := err.(type) {
- case *JSONClientError:
- return je.Code
- }
- return -1
- }
- func ErrorMsg(err error) string {
- if err == nil {
- return ""
- }
- switch je := err.(type) {
- case *JSONClientError:
- return je.Details
- }
- return err.Error()
- }
- func GetAddrPort(urlStr string) (string, int, error) {
- parts, err := url.Parse(urlStr)
- if err != nil {
- return "", 0, errors.Wrapf(err, "url.Parse %s", urlStr)
- }
- portStr := parts.Port()
- if len(portStr) == 0 {
- switch parts.Scheme {
- case "http":
- return parts.Hostname(), 80, nil
- case "https":
- return parts.Hostname(), 443, nil
- default:
- return "", 0, errors.Errorf("Unknown schema %s", parts.Scheme)
- }
- }
- port, err := strconv.Atoi(portStr)
- if err != nil {
- return "", 0, errors.Wrapf(err, "strconv.Atoi port string %s", portStr)
- }
- return parts.Hostname(), port, nil
- }
- func GetTransport(insecure bool) *http.Transport {
- return getTransport(insecure, false, 0)
- }
- func GetAdaptiveTransport(insecure bool) *http.Transport {
- return getTransport(insecure, true, 0)
- }
- func adptiveDial(ctx context.Context, network, addr string) (net.Conn, error) {
- conn, err := net.DialTimeout(network, addr, ConnectionTimeoutSeconds*time.Second)
- if err != nil {
- return nil, err
- }
- return getConnDelegate(conn, 10*time.Second, 20*time.Second), nil
- }
- func getTransport(insecure bool, adaptive bool, timeout time.Duration) *http.Transport {
- tr := &http.Transport{
- Proxy: http.ProxyFromEnvironment,
- // 一个空闲连接保持连接的时间
- // IdleConnTimeout is the maximum amount of time an idle
- // (keep-alive) connection will remain idle before closing
- // itself.
- // Zero means no limit.
- IdleConnTimeout: IdleConnTimeoutSeconds * time.Second,
- // 建立TCP连接后,等待TLS握手的超时时间
- // TLSHandshakeTimeout specifies the maximum amount of time waiting to
- // wait for a TLS handshake. Zero means no timeout.
- TLSHandshakeTimeout: TLSHandshakeTimeoutSeconds * time.Second,
- // 发送请求后,等待服务端http响应的超时时间
- // ResponseHeaderTimeout, if non-zero, specifies the amount of
- // time to wait for a server's response headers after fully
- // writing the request (including its body, if any). This
- // time does not include the time to read the response body.
- ResponseHeaderTimeout: ResponseHeaderTimeoutSeconds * time.Second,
- // 当请求携带Expect: 100-continue时,等待服务端100响应的超时时间
- // ExpectContinueTimeout, if non-zero, specifies the amount of
- // time to wait for a server's first response headers after fully
- // writing the request headers if the request has an
- // "Expect: 100-continue" header. Zero means no timeout and
- // causes the body to be sent immediately, without
- // waiting for the server to approve.
- // This time does not include the time to send the request header.
- ExpectContinueTimeout: 5 * time.Second,
- TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
- }
- if adaptive {
- tr.DialContext = adptiveDial
- } else {
- tr.IdleConnTimeout = timeout
- tr.TLSHandshakeTimeout = timeout
- tr.ResponseHeaderTimeout = timeout
- tr.DialContext = (&net.Dialer{
- // 建立TCP连接超时时间
- // Timeout is the maximum amount of time a dial will wait for
- // a connect to complete. If Deadline is also set, it may fail
- // earlier.
- //
- // The default is no timeout.
- //
- // When using TCP and dialing a host name with multiple IP
- // addresses, the timeout may be divided between them.
- //
- // With or without a timeout, the operating system may impose
- // its own earlier timeout. For instance, TCP timeouts are
- // often around 3 minutes.
- Timeout: ConnectionTimeoutSeconds * time.Second,
- //
- // KeepAlive specifies the interval between keep-alive
- // probes for an active network connection.
- // If zero, keep-alive probes are sent with a default value
- // (currently 15 seconds), if supported by the protocol and operating
- // system. Network protocols or operating systems that do
- // not support keep-alives ignore this field.
- // If negative, keep-alive probes are disabled.
- KeepAlive: 5 * time.Second, // send keep-alive probe every 5 seconds
- }).DialContext
- }
- return tr
- }
- func GetClient(insecure bool, timeout time.Duration) *http.Client {
- adaptive := false
- if timeout == 0 {
- adaptive = true
- }
- tr := getTransport(insecure, adaptive, timeout)
- return &http.Client{
- Transport: tr,
- // 一个完整http request的超时时间
- // Timeout specifies a time limit for requests made by this
- // Client. The timeout includes connection time, any
- // redirects, and reading the response body. The timer remains
- // running after Get, Head, Post, or Do return and will
- // interrupt reading of the Response.Body.
- //
- // A Timeout of zero means no timeout.
- //
- // The Client cancels requests to the underlying Transport
- // as if the Request's Context ended.
- //
- // For compatibility, the Client will also use the deprecated
- // CancelRequest method on Transport if found. New
- // RoundTripper implementations should use the Request's Context
- // for cancellation instead of implementing CancelRequest.
- Timeout: timeout,
- }
- }
- type TransportProxyFunc func(*http.Request) (*url.URL, error)
- func SetClientProxyFunc(
- client *http.Client,
- proxyFunc TransportProxyFunc,
- ) bool {
- set := false
- if transport, ok := client.Transport.(*http.Transport); ok {
- transport.Proxy = proxyFunc
- set = true
- }
- return set
- }
- func GetTimeoutClient(timeout time.Duration) *http.Client {
- return GetClient(true, timeout)
- }
- func GetAdaptiveTimeoutClient() *http.Client {
- return GetClient(true, 0)
- }
- var defaultHttpClient *http.Client
- func init() {
- defaultHttpClient = GetDefaultClient()
- }
- func GetDefaultClient() *http.Client {
- return GetClient(true, time.Second*15)
- }
- func getClientErrorClass(err error) error {
- cause := errors.Cause(err)
- if urlErr, ok := cause.(*url.Error); ok {
- if netErr, ok := urlErr.Err.(*net.OpError); ok {
- switch t := netErr.Err.(type) {
- case *net.DNSError:
- return errors.ErrDNS
- case *os.SyscallError:
- if errno, ok := t.Err.(syscall.Errno); ok {
- switch errno {
- case syscall.ECONNREFUSED:
- return errors.ErrConnectRefused
- case syscall.ETIMEDOUT:
- return errors.ErrTimeout
- }
- }
- }
- }
- }
- return errors.ErrClient
- }
- func isHTTPReqErrorRetryable(err error) bool {
- if err == nil {
- return false
- }
- switch e := err.(type) {
- case *url.Error:
- switch e.Err.(type) {
- case *net.DNSError, *net.OpError, net.UnknownNetworkError:
- return true
- }
- if strings.Contains(err.Error(), "Connection closed by foreign host") {
- return true
- } else if strings.Contains(err.Error(), "net/http: TLS handshake timeout") {
- // If error is - tlsHandshakeTimeoutError, retry.
- return true
- } else if strings.Contains(err.Error(), "i/o timeout") {
- // If error is - tcp timeoutError, retry.
- return true
- } else if strings.Contains(err.Error(), "connection timed out") {
- // If err is a net.Dial timeout, retry.
- return true
- } else if strings.Contains(err.Error(), "net/http: HTTP/1.x transport connection broken") {
- // If error is transport connection broken, retry.
- return true
- } else if strings.Contains(err.Error(), "net/http: timeout awaiting response headers") {
- // Retry errors due to server not sending the response before timeout
- return true
- } else if strings.Contains(err.Error(), "dial tcp: lookup") {
- return true
- }
- }
- return false
- }
- func Request(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body io.Reader, debug bool) (*http.Response, error) {
- return request(client, ctx, method, urlStr, header, body, false, debug)
- }
- func RequestWithRetry(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body io.Reader, debug bool) (*http.Response, error) {
- return request(client, ctx, method, urlStr, header, body, true, debug)
- }
- func request(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body io.Reader, retry, debug bool) (*http.Response, error) {
- req, resp, err := requestInternal(client, ctx, method, urlStr, header, body, retry, debug)
- if err != nil {
- var reqBody string
- if bodySeeker, ok := body.(io.ReadSeeker); ok {
- bodySeeker.Seek(0, io.SeekStart)
- reqBodyBytes, _ := io.ReadAll(bodySeeker)
- if reqBodyBytes != nil {
- reqBody = string(reqBodyBytes)
- }
- }
- if req == nil {
- ce := newJsonClientErrorFromRequest2(string(method), urlStr, header, reqBody)
- ce.Class = getClientErrorClass(err).Error()
- ce.Details = err.Error()
- ce.Code = 499
- return nil, ce
- }
- ce := newJsonClientErrorFromRequest(req, reqBody)
- ce.Class = getClientErrorClass(err).Error()
- ce.Details = err.Error()
- ce.Code = 499
- return nil, ce
- }
- return resp, nil
- }
- func requestInternal(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body io.Reader, retry, debug bool) (*http.Request, *http.Response, error) {
- if client == nil {
- client = defaultHttpClient
- }
- if header == nil {
- header = http.Header{}
- }
- ctxData := appctx.FetchAppContextData(ctx)
- var clientTrace *trace.STrace
- if len(ctxData.ServiceName) > 0 {
- if !ctxData.Trace.IsZero() {
- clientTrace = &ctxData.Trace
- }
- addr, port, err := GetAddrPort(urlStr)
- if err != nil {
- return nil, nil, err
- }
- clientTrace = trace.StartClientTrace(clientTrace, addr, port, ctxData.ServiceName)
- clientTrace.AddClientRequestHeader(header)
- }
- if len(ctxData.RequestId) > 0 {
- header.Set("X-Request-Id", ctxData.RequestId)
- }
- req, err := http.NewRequest(string(method), urlStr, body)
- if err != nil {
- return nil, nil, err
- }
- req.Header.Set("User-Agent", USER_AGENT)
- req.Header.Set("Accept", "*/*")
- req.Header.Set("Accept-Encoding", "*")
- if body == nil {
- if method != GET && method != HEAD {
- req.ContentLength = 0
- req.Header.Set("Content-Length", "0")
- }
- } else {
- clen := header.Get("Content-Length")
- if len(clen) > 0 {
- req.ContentLength, _ = strconv.ParseInt(clen, 10, 64)
- }
- }
- if header != nil {
- for k, vs := range header {
- for i, v := range vs {
- if i == 0 {
- req.Header.Set(k, v)
- } else {
- req.Header.Add(k, v)
- }
- }
- }
- }
- if debug {
- dump, _ := httputil.DumpRequestOut(req, false)
- yellow(string(dump))
- // 忽略掉上传文件的请求,避免大量日志输出
- if header.Get("Content-Type") != "application/octet-stream" {
- curlCmd, _ := http2curl.GetCurlCommand(req)
- cyan("CURL:", curlCmd, "\n")
- }
- }
- resp, err := func() (*http.Response, error) {
- var resp *http.Response
- for i := 0; i < 3; i++ {
- resp, err = client.Do(req)
- if err == nil || !retry || !isHTTPReqErrorRetryable(err) {
- return resp, err
- }
- time.Sleep(time.Second * 5)
- }
- return resp, err
- }()
- if err != nil {
- red(err.Error())
- return req, nil, err
- }
- encoding := resp.Header.Get("Content-Encoding")
- switch encoding {
- case "", "identity":
- // do nothing
- case "gzip":
- gzipBody, err := gzip.NewReader(resp.Body)
- if err != nil {
- return req, nil, errors.Wrap(err, "gzip.NewReader")
- }
- resp.Body = gzipBody
- case "deflate":
- resp.Body = flate.NewReader(resp.Body)
- default:
- return req, nil, errors.Wrapf(errors.ErrNotSupported, "unsupported content-encoding %s", encoding)
- }
- if clientTrace != nil {
- clientTrace.EndClientTraceHeader(resp.Header)
- }
- return req, resp, nil
- }
- func JSONRequestWithRetry(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body jsonutils.JSONObject, debug bool) (http.Header, jsonutils.JSONObject, error) {
- return jsonRequest(client, ctx, method, urlStr, header, body, true, debug)
- }
- func JSONRequest(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body jsonutils.JSONObject, debug bool) (http.Header, jsonutils.JSONObject, error) {
- return jsonRequest(client, ctx, method, urlStr, header, body, false, debug)
- }
- func jsonRequest(client sClient, ctx context.Context, method THttpMethod, urlStr string, header http.Header, body jsonutils.JSONObject, retry, debug bool) (http.Header, jsonutils.JSONObject, error) {
- var bodystr string
- if !gotypes.IsNil(body) {
- bodystr = body.String()
- }
- jbody := strings.NewReader(bodystr)
- if header == nil {
- header = http.Header{}
- }
- header.Set("Content-Length", strconv.FormatInt(int64(len(bodystr)), 10))
- header.Set("Content-Type", "application/json")
- resp, err := request(client, ctx, method, urlStr, header, jbody, retry, debug)
- return ParseJSONResponse(bodystr, resp, err, debug)
- }
- // closeResponse close non nil response with any response Body.
- // convenient wrapper to drain any remaining data on response body.
- //
- // Subsequently this allows golang http RoundTripper
- // to re-use the same connection for future requests.
- func CloseResponse(resp *http.Response) {
- // Callers should close resp.Body when done reading from it.
- // If resp.Body is not closed, the Client's underlying RoundTripper
- // (typically Transport) may not be able to re-use a persistent TCP
- // connection to the server for a subsequent "keep-alive" request.
- if resp != nil && resp.Body != nil {
- // Drain any remaining Body and then close the connection.
- // Without this closing connection would disallow re-using
- // the same connection for future uses.
- // - http://stackoverflow.com/a/17961593/4465767
- io.Copy(ioutil.Discard, resp.Body)
- resp.Body.Close()
- }
- }
- func (client *JsonClient) Send(ctx context.Context, req JsonRequest, response JsonResponse, debug bool) (http.Header, jsonutils.JSONObject, error) {
- var bodystr string
- body := req.GetRequestBody()
- if !gotypes.IsNil(body) {
- bodystr = body.String()
- }
- jbody := strings.NewReader(bodystr)
- resp, err := Request(client.client, ctx, req.GetHttpMethod(), req.GetUrl(), req.GetHeader(), jbody, debug)
- if err != nil {
- return nil, nil, err
- }
- defer CloseResponse(resp)
- if debug {
- dump, _ := httputil.DumpResponse(resp, false)
- if resp.StatusCode < 300 {
- green(string(dump))
- } else if resp.StatusCode < 400 {
- yellow(string(dump))
- } else {
- red(string(dump))
- }
- }
- rbody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- ce := newJsonClientErrorFromRequest(resp.Request, bodystr)
- ce.Code = resp.StatusCode
- ce.Class = string(errors.ErrClient)
- ce.Details = fmt.Sprintf("Fail to read body: %v", err)
- return resp.Header, nil, ce
- } else if debug {
- fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody))
- }
- rbody = bytes.TrimSpace(rbody)
- var jrbody jsonutils.JSONObject = nil
- if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') {
- var err error
- jrbody, err = jsonutils.Parse(rbody)
- if err != nil {
- if debug {
- fmt.Fprintf(os.Stderr, "parsing json %s failed: %v", string(rbody), err)
- }
- ce := newJsonClientErrorFromRequest(resp.Request, bodystr)
- ce.Code = resp.StatusCode
- ce.Class = string(errors.ErrServer)
- ce.Details = fmt.Sprintf("jsonutils.Parse(%s) error: %v", string(rbody), err)
- return resp.Header, nil, ce
- }
- }
- if resp.StatusCode < 300 {
- return resp.Header, jrbody, nil
- } else if resp.StatusCode >= 300 && resp.StatusCode < 400 {
- ce := JSONClientError{}
- ce.Code = resp.StatusCode
- ce.Details = resp.Header.Get("Location")
- ce.Class = "redirect"
- return resp.Header, jrbody, &ce
- }
- return resp.Header, jrbody, response.ParseErrorFromJsonResponse(resp.StatusCode, resp.Status, jrbody)
- }
- func IsRedirectError(err error) bool {
- ce, ok := err.(*JSONClientError)
- if ok && ce.Class == "redirect" {
- return true
- }
- return false
- }
- func ParseResponse(reqBody string, resp *http.Response, err error, debug bool) (http.Header, []byte, error) {
- if err != nil {
- return nil, nil, err
- }
- defer CloseResponse(resp)
- if debug {
- dump, _ := httputil.DumpResponse(resp, false)
- if resp.StatusCode < 300 {
- green(string(dump))
- } else if resp.StatusCode < 400 {
- yellow(string(dump))
- } else {
- red(string(dump))
- }
- }
- rbody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- ce.Code = 499
- ce.Details = fmt.Sprintf("Fail to read body: %s", err)
- ce.Class = string(errors.ErrClient)
- return resp.Header, nil, ce
- } else if debug {
- fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody))
- }
- if resp.StatusCode < 300 {
- return resp.Header, rbody, nil
- } else if resp.StatusCode >= 300 && resp.StatusCode < 400 {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- ce.Code = resp.StatusCode
- ce.Details = resp.Header.Get("Location")
- ce.Class = "redirect"
- return resp.Header, rbody, ce
- } else {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- ce.Code = resp.StatusCode
- ce.Details = resp.Status
- if len(rbody) > 0 {
- ce.Details = string(rbody)
- }
- return nil, nil, ce
- }
- }
- func ParseJSONResponse(reqBody string, resp *http.Response, err error, debug bool) (http.Header, jsonutils.JSONObject, error) {
- if err != nil {
- return nil, nil, err
- }
- defer CloseResponse(resp)
- if debug {
- dump, _ := httputil.DumpResponse(resp, false)
- if resp.StatusCode < 300 {
- green(string(dump))
- } else if resp.StatusCode < 400 {
- yellow(string(dump))
- } else {
- red(string(dump))
- }
- }
- rbody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- ce.Code = 499
- ce.Class = string(errors.ErrClient)
- ce.Details = fmt.Sprintf("Fail to read body: %s", err)
- return resp.Header, nil, ce
- } else if debug {
- fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody))
- }
- rbody = bytes.TrimSpace(rbody)
- var jrbody jsonutils.JSONObject = nil
- if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') {
- var err error
- jrbody, err = jsonutils.Parse(rbody)
- if err != nil && debug {
- // ignore the error
- fmt.Fprintf(os.Stderr, "parsing json failed: %s", err)
- }
- }
- if resp.StatusCode < 300 {
- return resp.Header, jrbody, nil
- } else if resp.StatusCode >= 300 && resp.StatusCode < 400 {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- ce.Code = resp.StatusCode
- ce.Details = resp.Header.Get("Location")
- ce.Class = "redirect"
- return resp.Header, jrbody, ce
- } else {
- ce := newJsonClientErrorFromRequest(resp.Request, reqBody)
- if jrbody == nil {
- ce.Code = resp.StatusCode
- ce.Details = resp.Status
- if len(rbody) > 0 {
- ce.Details = string(rbody)
- }
- return nil, nil, ce
- }
- err = jrbody.Unmarshal(ce)
- if len(ce.Class) > 0 && ce.Code >= 400 && len(ce.Details) > 0 {
- return nil, nil, ce
- }
- jrbody1, err := jrbody.GetMap()
- if err != nil {
- err = jrbody.Unmarshal(ce)
- if err != nil {
- ce.Details = err.Error()
- }
- return nil, nil, ce
- }
- var jrbody2 jsonutils.JSONObject
- if len(jrbody1) > 1 {
- jrbody2 = jsonutils.Marshal(jrbody1)
- } else {
- for _, v := range jrbody1 {
- jrbody2 = v
- }
- }
- if jrbody2 != nil {
- if ecode, _ := jrbody2.GetString("code"); len(ecode) > 0 {
- code, err := strconv.Atoi(ecode)
- if err != nil {
- ce.Class = ecode
- } else {
- ce.Code = code
- }
- }
- }
- if ce.Code == 0 {
- ce.Code = resp.StatusCode
- }
- if edetail := jsonutils.GetAnyString(jrbody2, []string{"message", "detail", "details", "error_msg"}); len(edetail) > 0 {
- ce.Details = edetail
- }
- if eclass := jsonutils.GetAnyString(jrbody2, []string{"title", "type", "error_code"}); len(eclass) > 0 {
- ce.Class = eclass
- }
- return nil, nil, ce
- }
- }
- func JoinPath(ep string, paths ...string) string {
- buf := strings.Builder{}
- buf.WriteString(strings.TrimRight(ep, "/"))
- for _, path := range paths {
- buf.WriteByte('/')
- buf.WriteString(strings.Trim(path, "/"))
- }
- return buf.String()
- }
|