agola/internal/util/http.go
Simone Gotti c1da3ab566 *: Improve error handling
* Create an APIError that should only be used for api returned errors.
  It'll wrap an error and can have different Kinds and optional code and
  message.
* The http handlers will use the first APIError available in the
  error chain and generate a json response body containing the code and
  the user message. The wrapped error is internal and is not sent in the
  response.
  If no api error is available in the chain a generic internal
  server error will be returned.
* Add a RemoteError type that will be created from remote services calls
  (runservice, configstore). It's similar to the APIError but a
  different type to not propagate to the caller response and it'll not
  contain any wrapped error.
* Gateway: when we call a remote service, by default, we'll create a
  APIError using the RemoteError Kind (omitting the code and the
  message that usually must not be propagated).
  This is done for all the remote service calls as a starting point, in
  future, if this default behavior is not the right one for a specific
  remote service call, a new api error with a different kind and/or
  augmented with the calling service error codes and user messages could
  be created.
* datamanager: Use a dedicated ErrNotExist (and converting objectstorage
  ErrNotExist).
2022-02-25 16:11:19 +01:00

125 lines
2.4 KiB
Go

package util
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
errors "golang.org/x/xerrors"
)
func HTTPResponse(w http.ResponseWriter, code int, res interface{}) error {
w.Header().Set("Content-Type", "application/json")
if res != nil {
resj, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
w.WriteHeader(code)
_, err = w.Write(resj)
return err
}
w.WriteHeader(code)
return nil
}
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
func ErrorResponseFromError(err error) *ErrorResponse {
if err == nil {
return nil
}
var derr *APIError
if errors.As(err, &derr) {
return &ErrorResponse{Code: string(derr.Code), Message: derr.Message}
}
// on generic error return an error response without any code
return &ErrorResponse{}
}
func HTTPError(w http.ResponseWriter, err error) bool {
if err == nil {
return false
}
response := ErrorResponseFromError(err)
resj, merr := json.Marshal(response)
if merr != nil {
w.WriteHeader(http.StatusInternalServerError)
return true
}
code := http.StatusInternalServerError
var derr *APIError
if errors.As(err, &derr) {
switch derr.Kind {
case ErrBadRequest:
code = http.StatusBadRequest
case ErrNotExist:
code = http.StatusNotFound
case ErrForbidden:
code = http.StatusForbidden
case ErrUnauthorized:
code = http.StatusUnauthorized
case ErrInternal:
code = http.StatusInternalServerError
}
}
w.WriteHeader(code)
_, _ = w.Write(resj)
return true
}
func ErrFromRemote(resp *http.Response) error {
if resp == nil {
return nil
}
if resp.StatusCode/100 == 2 {
return nil
}
response := &ErrorResponse{}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// Re-populate error response body so it can be parsed by the caller if needed
resp.Body = ioutil.NopCloser(bytes.NewBuffer(data))
if err := json.Unmarshal(data, &response); err != nil {
return errors.Errorf("unknown api error (status: %d)", resp.StatusCode)
}
kind := ErrInternal
switch resp.StatusCode {
case http.StatusBadRequest:
kind = ErrBadRequest
case http.StatusNotFound:
kind = ErrNotExist
case http.StatusForbidden:
kind = ErrForbidden
case http.StatusUnauthorized:
kind = ErrUnauthorized
case http.StatusInternalServerError:
kind = ErrInternal
}
return NewRemoteError(kind, response.Code, response.Message)
}