github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/utils/http.go (about)

     1  /*
     2   * Copyright (c) 2020-present unTill Pro, Ltd.
     3   * @author Denis Gribanov
     4   */
     5  
     6  package coreutils
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"net"
    16  	"net/http"
    17  	"net/url"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/stretchr/testify/require"
    23  	"github.com/voedger/voedger/pkg/goutils/logger"
    24  	"golang.org/x/exp/slices"
    25  
    26  	ibus "github.com/voedger/voedger/staging/src/github.com/untillpro/airs-ibus"
    27  )
    28  
    29  func NewHTTPErrorf(httpStatus int, args ...interface{}) SysError {
    30  	return SysError{
    31  		HTTPStatus: httpStatus,
    32  		Message:    fmt.Sprint(args...),
    33  	}
    34  }
    35  
    36  func NewHTTPError(httpStatus int, err error) SysError {
    37  	return NewHTTPErrorf(httpStatus, err.Error())
    38  }
    39  
    40  func ReplyErrf(sender ibus.ISender, status int, args ...interface{}) {
    41  	ReplyErrDef(sender, NewHTTPErrorf(status, args...), http.StatusInternalServerError)
    42  }
    43  
    44  //nolint:errorlint
    45  func ReplyErrDef(sender ibus.ISender, err error, defaultStatusCode int) {
    46  	res := WrapSysError(err, defaultStatusCode).(SysError)
    47  	ReplyJSON(sender, res.HTTPStatus, res.ToJSON())
    48  }
    49  
    50  func ReplyErr(sender ibus.ISender, err error) {
    51  	ReplyErrDef(sender, err, http.StatusInternalServerError)
    52  }
    53  
    54  func ReplyJSON(sender ibus.ISender, httpCode int, body string) {
    55  	sender.SendResponse(ibus.Response{
    56  		ContentType: ApplicationJSON,
    57  		StatusCode:  httpCode,
    58  		Data:        []byte(body),
    59  	})
    60  }
    61  
    62  func ReplyBadRequest(sender ibus.ISender, message string) {
    63  	ReplyErrf(sender, http.StatusBadRequest, message)
    64  }
    65  
    66  func replyAccessDenied(sender ibus.ISender, code int, message string) {
    67  	msg := "access denied"
    68  	if len(message) > 0 {
    69  		msg += ": " + message
    70  	}
    71  	ReplyErrf(sender, code, msg)
    72  }
    73  
    74  func ReplyAccessDeniedUnauthorized(sender ibus.ISender, message string) {
    75  	replyAccessDenied(sender, http.StatusUnauthorized, message)
    76  }
    77  
    78  func ReplyAccessDeniedForbidden(sender ibus.ISender, message string) {
    79  	replyAccessDenied(sender, http.StatusForbidden, message)
    80  }
    81  
    82  func ReplyUnauthorized(sender ibus.ISender, message string) {
    83  	ReplyErrf(sender, http.StatusUnauthorized, message)
    84  }
    85  
    86  func ReplyInternalServerError(sender ibus.ISender, message string, err error) {
    87  	ReplyErrf(sender, http.StatusInternalServerError, message, ": ", err)
    88  }
    89  
    90  // WithResponseHandler, WithLongPolling and WithDiscardResponse are mutual exclusive
    91  func WithResponseHandler(responseHandler func(httpResp *http.Response)) ReqOptFunc {
    92  	return func(ro *reqOpts) {
    93  		ro.responseHandler = responseHandler
    94  	}
    95  }
    96  
    97  // WithLongPolling, WithResponseHandler and WithDiscardResponse are mutual exclusive
    98  func WithLongPolling() ReqOptFunc {
    99  	return func(ro *reqOpts) {
   100  		ro.responseHandler = func(resp *http.Response) {
   101  			if !slices.Contains(ro.expectedHTTPCodes, resp.StatusCode) {
   102  				body, err := readBody(resp)
   103  				if err != nil {
   104  					panic("failed to read response body in custom response handler: " + err.Error())
   105  				}
   106  				panic(fmt.Sprintf("actual status code %d, expected %v. Body: %s", resp.StatusCode, ro.expectedHTTPCodes, body))
   107  			}
   108  		}
   109  	}
   110  }
   111  
   112  // WithDiscardResponse, WithResponseHandler and WithLongPolling are mutual exclusive
   113  // causes FederationReq() to return nil for *HTTPResponse
   114  func WithDiscardResponse() ReqOptFunc {
   115  	return func(opts *reqOpts) {
   116  		opts.discardResp = true
   117  	}
   118  }
   119  
   120  func WithCookies(cookiesPairs ...string) ReqOptFunc {
   121  	return func(po *reqOpts) {
   122  		for i := 0; i < len(cookiesPairs); i += 2 {
   123  			po.cookies[cookiesPairs[i]] = cookiesPairs[i+1]
   124  		}
   125  	}
   126  }
   127  
   128  func WithHeaders(headersPairs ...string) ReqOptFunc {
   129  	return func(po *reqOpts) {
   130  		for i := 0; i < len(headersPairs); i += 2 {
   131  			po.headers[headersPairs[i]] = headersPairs[i+1]
   132  		}
   133  	}
   134  }
   135  
   136  func WithExpectedCode(expectedHTTPCode int, expectErrorContains ...string) ReqOptFunc {
   137  	return func(po *reqOpts) {
   138  		po.expectedHTTPCodes = append(po.expectedHTTPCodes, expectedHTTPCode)
   139  		po.expectedErrorContains = append(po.expectedErrorContains, expectErrorContains...)
   140  	}
   141  }
   142  
   143  // has priority over WithAuthorizeByIfNot
   144  func WithAuthorizeBy(principalToken string) ReqOptFunc {
   145  	return func(po *reqOpts) {
   146  		po.headers[Authorization] = BearerPrefix + principalToken
   147  	}
   148  }
   149  
   150  func WithRetryOnCertainError(errMatcher func(err error) bool, timeout time.Duration, retryDelay time.Duration) ReqOptFunc {
   151  	return func(opts *reqOpts) {
   152  		opts.retriersOnErrors = append(opts.retriersOnErrors, retrier{
   153  			macther: errMatcher,
   154  			timeout: timeout,
   155  			delay:   retryDelay,
   156  		})
   157  	}
   158  }
   159  
   160  func WithRetryOnAnyError(timeout time.Duration, retryDelay time.Duration) ReqOptFunc {
   161  	return WithRetryOnCertainError(func(error) bool { return true }, timeout, retryDelay)
   162  }
   163  
   164  func WithAuthorizeByIfNot(principalToken string) ReqOptFunc {
   165  	return func(po *reqOpts) {
   166  		if _, ok := po.headers[Authorization]; !ok {
   167  			po.headers[Authorization] = BearerPrefix + principalToken
   168  		}
   169  	}
   170  }
   171  
   172  func WithRelativeURL(relativeURL string) ReqOptFunc {
   173  	return func(ro *reqOpts) {
   174  		ro.relativeURL = relativeURL
   175  	}
   176  }
   177  
   178  func WithMethod(m string) ReqOptFunc {
   179  	return func(po *reqOpts) {
   180  		po.method = m
   181  	}
   182  }
   183  
   184  func Expect409(expected ...string) ReqOptFunc {
   185  	return WithExpectedCode(http.StatusConflict, expected...)
   186  }
   187  
   188  func Expect404() ReqOptFunc {
   189  	return WithExpectedCode(http.StatusNotFound)
   190  }
   191  
   192  func Expect401() ReqOptFunc {
   193  	return WithExpectedCode(http.StatusUnauthorized)
   194  }
   195  
   196  func Expect403(expectedMessages ...string) ReqOptFunc {
   197  	return WithExpectedCode(http.StatusForbidden, expectedMessages...)
   198  }
   199  
   200  func Expect400(expectErrorContains ...string) ReqOptFunc {
   201  	return WithExpectedCode(http.StatusBadRequest, expectErrorContains...)
   202  }
   203  
   204  func Expect400RefIntegrity_Existence() ReqOptFunc {
   205  	return WithExpectedCode(http.StatusBadRequest, "referential integrity violation", "does not exist")
   206  }
   207  
   208  func Expect400RefIntegrity_QName() ReqOptFunc {
   209  	return WithExpectedCode(http.StatusBadRequest, "referential integrity violation", "QNames are only allowed")
   210  }
   211  
   212  func Expect429() ReqOptFunc {
   213  	return WithExpectedCode(http.StatusTooManyRequests)
   214  }
   215  
   216  func Expect500() ReqOptFunc {
   217  	return WithExpectedCode(http.StatusInternalServerError)
   218  }
   219  
   220  func Expect503() ReqOptFunc {
   221  	return WithExpectedCode(http.StatusServiceUnavailable)
   222  }
   223  
   224  func Expect410() ReqOptFunc {
   225  	return WithExpectedCode(http.StatusGone)
   226  }
   227  
   228  func ExpectSysError500() ReqOptFunc {
   229  	return func(opts *reqOpts) {
   230  		opts.expectedSysErrorCode = http.StatusInternalServerError
   231  	}
   232  }
   233  
   234  type reqOpts struct {
   235  	method                string
   236  	headers               map[string]string
   237  	cookies               map[string]string
   238  	expectedHTTPCodes     []int
   239  	expectedErrorContains []string
   240  	responseHandler       func(httpResp *http.Response) // used if no errors and an expected status code is received
   241  	relativeURL           string
   242  	discardResp           bool
   243  	expectedSysErrorCode  int
   244  	retriersOnErrors      []retrier
   245  }
   246  
   247  func req(method, url, body string, headers, cookies map[string]string) (*http.Request, error) {
   248  	req, err := http.NewRequest(method, url, bytes.NewReader([]byte(body)))
   249  	if err != nil {
   250  		return nil, fmt.Errorf("NewRequest() failed: %w", err)
   251  	}
   252  	req.Close = true
   253  	for k, v := range headers {
   254  		req.Header.Add(k, v)
   255  	}
   256  	for k, v := range cookies {
   257  		req.AddCookie(&http.Cookie{
   258  			Name:  k,
   259  			Value: v,
   260  		})
   261  	}
   262  	return req, nil
   263  }
   264  
   265  // status code expected -> DiscardBody, ResponseHandler are used
   266  // status code is unexpected -> DiscardBody, ResponseHandler are ignored, body is read out, wrapped ErrUnexpectedStatusCode is returned
   267  func (c *implIHTTPClient) Req(urlStr string, body string, optFuncs ...ReqOptFunc) (*HTTPResponse, error) {
   268  	opts := &reqOpts{
   269  		headers: map[string]string{},
   270  		cookies: map[string]string{},
   271  		method:  http.MethodGet,
   272  	}
   273  	optFuncs = append(optFuncs, WithRetryOnCertainError(func(err error) bool {
   274  		// https://github.com/voedger/voedger/issues/1694
   275  		return IsWSAEError(err, WSAECONNREFUSED)
   276  	}, retryOn_WSAECONNREFUSED_Timeout, retryOn_WSAECONNREFUSED_Delay))
   277  	for _, optFunc := range optFuncs {
   278  		optFunc(opts)
   279  	}
   280  
   281  	mutualExclusiveOpts := 0
   282  	if opts.discardResp {
   283  		mutualExclusiveOpts++
   284  	}
   285  	if opts.expectedSysErrorCode > 0 {
   286  		mutualExclusiveOpts++
   287  	}
   288  	if opts.responseHandler != nil {
   289  		mutualExclusiveOpts++
   290  	}
   291  	if mutualExclusiveOpts > 1 {
   292  		panic("request options conflict")
   293  	}
   294  
   295  	if len(opts.expectedHTTPCodes) == 0 {
   296  		opts.expectedHTTPCodes = append(opts.expectedHTTPCodes, http.StatusOK)
   297  	}
   298  	if len(opts.relativeURL) > 0 {
   299  		netURL, err := url.Parse(urlStr)
   300  		if err != nil {
   301  			return nil, err
   302  		}
   303  		netURL.Path = opts.relativeURL
   304  		urlStr = netURL.String()
   305  	}
   306  	var resp *http.Response
   307  	var err error
   308  	tryNum := 0
   309  	startTime := time.Now()
   310  
   311  reqLoop:
   312  	for time.Since(startTime) < maxHTTPRequestTimeout {
   313  		req, err := req(opts.method, urlStr, body, opts.headers, opts.cookies)
   314  		if err != nil {
   315  			return nil, err
   316  		}
   317  		resp, err = c.client.Do(req)
   318  		if err != nil {
   319  			for _, retrier := range opts.retriersOnErrors {
   320  				if retrier.macther(err) {
   321  					if time.Since(startTime) < retrier.timeout {
   322  						time.Sleep(retrier.delay)
   323  						continue reqLoop
   324  					}
   325  				}
   326  			}
   327  			return nil, fmt.Errorf("request do() failed: %w", err)
   328  		}
   329  		if opts.responseHandler == nil {
   330  			defer resp.Body.Close()
   331  		}
   332  		if resp.StatusCode == http.StatusServiceUnavailable && !slices.Contains(opts.expectedHTTPCodes, http.StatusServiceUnavailable) {
   333  			if err := discardRespBody(resp); err != nil {
   334  				return nil, err
   335  			}
   336  			if tryNum > shortRetriesOn503Amount {
   337  				time.Sleep(longRetryOn503Delay)
   338  			} else {
   339  				time.Sleep(shortRetryOn503Delay)
   340  			}
   341  			logger.Verbose("503. retrying...")
   342  			tryNum++
   343  			continue
   344  		}
   345  		break
   346  	}
   347  	httpResponse := &HTTPResponse{
   348  		HTTPResp:             resp,
   349  		expectedSysErrorCode: opts.expectedSysErrorCode,
   350  		expectedHTTPCodes:    opts.expectedHTTPCodes,
   351  	}
   352  	isCodeExpected := slices.Contains(opts.expectedHTTPCodes, resp.StatusCode)
   353  	if isCodeExpected {
   354  		if opts.responseHandler != nil {
   355  			opts.responseHandler(resp)
   356  			return httpResponse, nil
   357  		}
   358  		if opts.discardResp {
   359  			err := discardRespBody(resp)
   360  			return nil, err
   361  		}
   362  	}
   363  	respBody, err := readBody(resp)
   364  	if err != nil {
   365  		return nil, fmt.Errorf("failed to read response body: %w", err)
   366  	}
   367  	httpResponse.Body = respBody
   368  	var statusErr error
   369  	if !isCodeExpected {
   370  		statusErr = fmt.Errorf("%w: %d, %s", ErrUnexpectedStatusCode, resp.StatusCode, respBody)
   371  	}
   372  	if resp.StatusCode != http.StatusOK && len(opts.expectedErrorContains) > 0 {
   373  		sysError := map[string]interface{}{}
   374  		if err := json.Unmarshal([]byte(respBody), &sysError); err != nil {
   375  			return nil, err
   376  		}
   377  		actualError := sysError["sys.Error"].(map[string]interface{})["Message"].(string)
   378  		if !containsAllMessages(opts.expectedErrorContains, actualError) {
   379  			return nil, fmt.Errorf(`actual error message "%s" does not contain the expected messages %v`, actualError, opts.expectedErrorContains)
   380  		}
   381  	}
   382  	return httpResponse, statusErr
   383  }
   384  
   385  func (c *implIHTTPClient) CloseIdleConnections() {
   386  	c.client.CloseIdleConnections()
   387  }
   388  
   389  func containsAllMessages(strs []string, toFind string) bool {
   390  	for _, str := range strs {
   391  		if !strings.Contains(toFind, str) {
   392  			return false
   393  		}
   394  	}
   395  	return true
   396  }
   397  
   398  func (resp *HTTPResponse) ExpectedSysErrorCode() int {
   399  	return resp.expectedSysErrorCode
   400  }
   401  
   402  func (resp *HTTPResponse) ExpectedHTTPCodes() []int {
   403  	return resp.expectedHTTPCodes
   404  }
   405  
   406  func (resp *HTTPResponse) Println() {
   407  	log.Println(resp.Body)
   408  }
   409  
   410  func (resp *HTTPResponse) getError(t *testing.T) map[string]interface{} {
   411  	t.Helper()
   412  	m := map[string]interface{}{}
   413  	err := json.Unmarshal([]byte(resp.Body), &m)
   414  	require.NoError(t, err)
   415  	return m["sys.Error"].(map[string]interface{})
   416  }
   417  
   418  func (resp *HTTPResponse) RequireError(t *testing.T, message string) {
   419  	t.Helper()
   420  	m := resp.getError(t)
   421  	require.Equal(t, message, m["Message"])
   422  }
   423  
   424  func (resp *HTTPResponse) RequireContainsError(t *testing.T, messagePart string) {
   425  	t.Helper()
   426  	m := resp.getError(t)
   427  	require.Contains(t, m["Message"], messagePart)
   428  }
   429  
   430  func readBody(resp *http.Response) (string, error) {
   431  	respBody, err := io.ReadAll(resp.Body)
   432  	return string(respBody), err
   433  }
   434  
   435  func discardRespBody(resp *http.Response) error {
   436  	_, err := io.Copy(io.Discard, resp.Body)
   437  	if err != nil {
   438  		// https://github.com/voedger/voedger/issues/1694
   439  		if !IsWSAEError(err, WSAECONNRESET) {
   440  			return fmt.Errorf("failed to discard response body: %w", err)
   441  		}
   442  	}
   443  	return nil
   444  }
   445  
   446  func (resp *FuncResponse) SectionRow(rowIdx ...int) []interface{} {
   447  	if len(rowIdx) > 1 {
   448  		panic("must be 0 or 1 rowIdx'es")
   449  	}
   450  	if len(resp.Sections) == 0 {
   451  		panic("empty response")
   452  	}
   453  	i := 0
   454  	if len(rowIdx) == 1 {
   455  		i = rowIdx[0]
   456  	}
   457  	return resp.Sections[0].Elements[i][0][0]
   458  }
   459  
   460  // returns a new ID for raw ID 1
   461  func (resp *FuncResponse) NewID() int64 {
   462  	return resp.NewIDs["1"]
   463  }
   464  
   465  func (resp *FuncResponse) IsEmpty() bool {
   466  	return len(resp.Sections) == 0
   467  }
   468  
   469  func (fe FuncError) Error() string {
   470  	if len(fe.ExpectedHTTPCodes) == 1 && fe.ExpectedHTTPCodes[0] == http.StatusOK {
   471  		return fmt.Sprintf("status %d: %s", fe.HTTPStatus, fe.Message)
   472  	}
   473  	return fmt.Sprintf("status %d, expected %v: %s", fe.HTTPStatus, fe.ExpectedHTTPCodes, fe.Message)
   474  }
   475  
   476  func (fe FuncError) Unwrap() error {
   477  	return fe.SysError
   478  }
   479  
   480  type implIHTTPClient struct {
   481  	client *http.Client
   482  }
   483  
   484  func NewIHTTPClient() (client IHTTPClient, clenup func()) {
   485  	// set linger - see https://github.com/voedger/voedger/issues/415
   486  	tr := http.DefaultTransport.(*http.Transport).Clone()
   487  	tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
   488  		dialer := net.Dialer{}
   489  		conn, err := dialer.DialContext(ctx, network, addr)
   490  		if err != nil {
   491  			return nil, err
   492  		}
   493  
   494  		err = conn.(*net.TCPConn).SetLinger(0)
   495  		return conn, err
   496  	}
   497  	client = &implIHTTPClient{client: &http.Client{Transport: tr}}
   498  	return client, client.CloseIdleConnections
   499  }