aboutsummaryrefslogtreecommitdiffstats
path: root/modules/private/request.go
blob: 3eb8c92c1ac02ab265c14d128fc4c81faf1f2cf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package private

import (
	"fmt"
	"io"
	"net/http"
	"unicode"

	"code.gitea.io/gitea/modules/httplib"
	"code.gitea.io/gitea/modules/json"
)

// responseText is used to get the response as text, instead of parsing it as JSON.
type responseText struct {
	Text string
}

// ResponseExtra contains extra information about the response, especially for error responses.
type ResponseExtra struct {
	StatusCode int
	UserMsg    string
	Error      error
}

type responseCallback func(resp *http.Response, extra *ResponseExtra)

func (re *ResponseExtra) HasError() bool {
	return re.Error != nil
}

type responseError struct {
	statusCode  int
	errorString string
}

func (re responseError) Error() string {
	if re.errorString == "" {
		return fmt.Sprintf("internal API error response, status=%d", re.statusCode)
	}
	return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString)
}

// requestJSONUserMsg sends a request to the gitea server and then parses the response.
// If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil,
// and the ResponseExtra.UserMsg field will be set to a message for the end user.
//
// * If the "res" is a struct pointer, the response will be parsed as JSON
// * If the "res" is responseText pointer, the response will be stored as text in it
// * If the "res" is responseCallback pointer, the callback function should set the ResponseExtra fields accordingly
func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra ResponseExtra) {
	resp, err := req.Response()
	if err != nil {
		extra.UserMsg = "Internal Server Connection Error"
		extra.Error = fmt.Errorf("unable to contact gitea %q: %w", req.GoString(), err)
		return nil, extra
	}
	defer resp.Body.Close()

	extra.StatusCode = resp.StatusCode

	// if the status code is not 2xx, try to parse the error response
	if resp.StatusCode/100 != 2 {
		var respErr Response
		if err := json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
			extra.UserMsg = "Internal Server Error Decoding Failed"
			extra.Error = fmt.Errorf("unable to decode error response %q: %w", req.GoString(), err)
			return nil, extra
		}
		extra.UserMsg = respErr.UserMsg
		if extra.UserMsg == "" {
			extra.UserMsg = "Internal Server Error (no message for end users)"
		}
		extra.Error = responseError{statusCode: resp.StatusCode, errorString: respErr.Err}
		return res, extra
	}

	// now, the StatusCode must be 2xx
	var v any = res
	if respText, ok := v.(*responseText); ok {
		// get the whole response as a text string
		bs, err := io.ReadAll(resp.Body)
		if err != nil {
			extra.UserMsg = "Internal Server Response Reading Failed"
			extra.Error = fmt.Errorf("unable to read response %q: %w", req.GoString(), err)
			return nil, extra
		}
		respText.Text = string(bs)
		return res, extra
	} else if callback, ok := v.(*responseCallback); ok {
		// pass the response to callback, and let the callback update the ResponseExtra
		extra.StatusCode = resp.StatusCode
		(*callback)(resp, &extra)
		return nil, extra
	} else if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
		// decode the response into the given struct
		extra.UserMsg = "Internal Server Response Decoding Failed"
		extra.Error = fmt.Errorf("unable to decode response %q: %w", req.GoString(), err)
		return nil, extra
	}

	if respMsg, ok := v.(*Response); ok {
		// if the "res" is Response structure, try to get the UserMsg from it and update the ResponseExtra
		extra.UserMsg = respMsg.UserMsg
		if respMsg.Err != "" {
			// usually this shouldn't happen, because the StatusCode is 2xx, there should be no error.
			// but we still handle the "err" response, in case some people return error messages by status code 200.
			extra.Error = responseError{statusCode: resp.StatusCode, errorString: respMsg.Err}
		}
	}

	return res, extra
}

// requestJSONUserMsg sends a request to the gitea server and then parses the response as private.Response
// If the request succeeds, the successMsg will be used as part of ResponseExtra.UserMsg.
func requestJSONUserMsg(req *httplib.Request, successMsg string) ResponseExtra {
	resp, extra := requestJSONResp(req, &Response{})
	if extra.HasError() {
		return extra
	}
	if resp.UserMsg == "" {
		extra.UserMsg = successMsg // if UserMsg is empty, then use successMsg as userMsg
	} else if successMsg != "" {
		// else, now UserMsg is not empty, if successMsg is not empty, then append successMsg to UserMsg
		if unicode.IsPunct(rune(extra.UserMsg[len(extra.UserMsg)-1])) {
			extra.UserMsg = extra.UserMsg + " " + successMsg
		} else {
			extra.UserMsg = extra.UserMsg + ". " + successMsg
		}
	}
	return extra
}