mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
129 lines
4.4 KiB
Go
129 lines
4.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package private
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/git/modules/httplib"
|
|
"github.com/hanzoai/git/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 forge 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 Hanzo Git %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 forge 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
|
|
}
|