code.gitea.io/gitea@v1.22.3/modules/private/request.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package private
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  
    11  	"code.gitea.io/gitea/modules/httplib"
    12  	"code.gitea.io/gitea/modules/json"
    13  )
    14  
    15  // ResponseText is used to get the response as text, instead of parsing it as JSON.
    16  type ResponseText struct {
    17  	Text string
    18  }
    19  
    20  // ResponseExtra contains extra information about the response, especially for error responses.
    21  type ResponseExtra struct {
    22  	StatusCode int
    23  	UserMsg    string
    24  	Error      error
    25  }
    26  
    27  type responseCallback struct {
    28  	Callback func(resp *http.Response, extra *ResponseExtra)
    29  }
    30  
    31  func (re *ResponseExtra) HasError() bool {
    32  	return re.Error != nil
    33  }
    34  
    35  type responseError struct {
    36  	statusCode  int
    37  	errorString string
    38  }
    39  
    40  func (re responseError) Error() string {
    41  	if re.errorString == "" {
    42  		return fmt.Sprintf("internal API error response, status=%d", re.statusCode)
    43  	}
    44  	return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString)
    45  }
    46  
    47  // requestJSONResp sends a request to the gitea server and then parses the response.
    48  // If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil,
    49  // and the ResponseExtra.UserMsg field will be set to a message for the end user.
    50  // Caller should check the ResponseExtra.HasError() first to see whether the request fails.
    51  //
    52  // * If the "res" is a struct pointer, the response will be parsed as JSON
    53  // * If the "res" is ResponseText pointer, the response will be stored as text in it
    54  // * If the "res" is responseCallback pointer, the callback function should set the ResponseExtra fields accordingly
    55  func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra ResponseExtra) {
    56  	resp, err := req.Response()
    57  	if err != nil {
    58  		extra.UserMsg = "Internal Server Connection Error"
    59  		extra.Error = fmt.Errorf("unable to contact gitea %q: %w", req.GoString(), err)
    60  		return nil, extra
    61  	}
    62  	defer resp.Body.Close()
    63  
    64  	extra.StatusCode = resp.StatusCode
    65  
    66  	// if the status code is not 2xx, try to parse the error response
    67  	if resp.StatusCode/100 != 2 {
    68  		var respErr Response
    69  		if err := json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
    70  			extra.UserMsg = "Internal Server Error Decoding Failed"
    71  			extra.Error = fmt.Errorf("unable to decode error response %q: %w", req.GoString(), err)
    72  			return nil, extra
    73  		}
    74  		extra.UserMsg = respErr.UserMsg
    75  		if extra.UserMsg == "" {
    76  			extra.UserMsg = "Internal Server Error (no message for end users)"
    77  		}
    78  		extra.Error = responseError{statusCode: resp.StatusCode, errorString: respErr.Err}
    79  		return res, extra
    80  	}
    81  
    82  	// now, the StatusCode must be 2xx
    83  	var v any = res
    84  	if respText, ok := v.(*ResponseText); ok {
    85  		// get the whole response as a text string
    86  		bs, err := io.ReadAll(resp.Body)
    87  		if err != nil {
    88  			extra.UserMsg = "Internal Server Response Reading Failed"
    89  			extra.Error = fmt.Errorf("unable to read response %q: %w", req.GoString(), err)
    90  			return nil, extra
    91  		}
    92  		respText.Text = string(bs)
    93  		return res, extra
    94  	} else if cb, ok := v.(*responseCallback); ok {
    95  		// pass the response to callback, and let the callback update the ResponseExtra
    96  		extra.StatusCode = resp.StatusCode
    97  		cb.Callback(resp, &extra)
    98  		return nil, extra
    99  	} else if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
   100  		// decode the response into the given struct
   101  		extra.UserMsg = "Internal Server Response Decoding Failed"
   102  		extra.Error = fmt.Errorf("unable to decode response %q: %w", req.GoString(), err)
   103  		return nil, extra
   104  	}
   105  
   106  	if respMsg, ok := v.(*Response); ok {
   107  		// if the "res" is Response structure, try to get the UserMsg from it and update the ResponseExtra
   108  		extra.UserMsg = respMsg.UserMsg
   109  		if respMsg.Err != "" {
   110  			// usually this shouldn't happen, because the StatusCode is 2xx, there should be no error.
   111  			// but we still handle the "err" response, in case some people return error messages by status code 200.
   112  			extra.Error = responseError{statusCode: resp.StatusCode, errorString: respMsg.Err}
   113  		}
   114  	}
   115  
   116  	return res, extra
   117  }
   118  
   119  // requestJSONClientMsg sends a request to the gitea server, server only responds text message status=200 with "success" body
   120  // If the request succeeds (200), the argument clientSuccessMsg will be used as ResponseExtra.UserMsg.
   121  func requestJSONClientMsg(req *httplib.Request, clientSuccessMsg string) ResponseExtra {
   122  	_, extra := requestJSONResp(req, &ResponseText{})
   123  	if extra.HasError() {
   124  		return extra
   125  	}
   126  	extra.UserMsg = clientSuccessMsg
   127  	return extra
   128  }