// Copyright 2023 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package private import ( "fmt" "io" "net/http" "forgejo.org/modules/httplib" "forgejo.org/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 struct { Callback 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)} // requestJSONResp 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.// Caller should check the ResponseExtra.HasError() first to see whether the request fails.//// * 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 accordinglyfunc 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 cb, ok := v.(*responseCallback); ok { // pass the response to callback, and let the callback update the ResponseExtra extra.StatusCode = resp.StatusCode cb.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} // requestJSONClientMsg sends a request to the gitea server, server only responds text message status=200 with "success" body// If the request succeeds (200), the argument clientSuccessMsg will be used as ResponseExtra.UserMsg.func requestJSONClientMsg(req *httplib.Request, clientSuccessMsg string) ResponseExtra { _, extra := requestJSONResp(req, &ResponseText{}) if extra.HasError() { return extra } extra.UserMsg = clientSuccessMsg return extra}
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"fmt"
"io"
"net/http"
"forgejo.org/modules/httplib"
"forgejo.org/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 struct {
Callback 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)
}
// requestJSONResp 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.
// Caller should check the ResponseExtra.HasError() first to see whether the request fails.
//
// * 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 cb, ok := v.(*responseCallback); ok {
// pass the response to callback, and let the callback update the ResponseExtra
extra.StatusCode = resp.StatusCode
cb.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
}
// requestJSONClientMsg sends a request to the gitea server, server only responds text message status=200 with "success" body
// If the request succeeds (200), the argument clientSuccessMsg will be used as ResponseExtra.UserMsg.
func requestJSONClientMsg(req *httplib.Request, clientSuccessMsg string) ResponseExtra {
_, extra := requestJSONResp(req, &ResponseText{})
if extra.HasError() {
return extra
}
extra.UserMsg = clientSuccessMsg
return extra
}