Newer
Older
minecraft-ui / internal / rest / decode.go
package rest

import (
	"net/http"
	"io"
	"io/ioutil"
	"encoding/json"
	"fmt"
)

// Bind decodes a request body and executes the Binder method of the
// payload structure.
func Bind(r *http.Request, v interface{}) error {
	return DecodeJSON(r, v)
}

// DecodeJSON decodes any request body as JSON that matches a JSON content-type.
func DecodeJSON(r *http.Request, v interface{}) error {
	h := r.Header.Get("Content-Type")
	if h == "" {
		h = "application/json"
	}

	ct := ParseContentType(h)
	if !(ct.Type == "application" && (ct.Subtype == "json" || ct.Suffix == "json")) {
		return ErrUnsupportedMediaType
	}

	if err := decodeJSON(r.Body, v); err != nil {
		return ErrMalformedPayload(err)
	}

	return nil
}

// decodeJSON decodes a request body as JSON and discards the remainder.
func decodeJSON(r io.Reader, v interface{}) error {
	defer func() {
		_, err := io.Copy(ioutil.Discard, r)
		fmt.Errorf("decodeJSON: %#v", err)
	}()
	return json.NewDecoder(r).Decode(v)
}

// CheckJSONResponse returns an error (of type *ResponseError) if the response
// status code is not 2xx.
//
// It gracefully degrades by ignoring errors while attemping to read the body.
// It is the caller's responsibility to close res.Body.
func CheckJSONResponse(res *http.Response) error {
	if res.StatusCode >= 200 && res.StatusCode <= 299 {
		return nil
	}
	body, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20)) // 1MiB
	if err == nil {
		var resterr Error
		err = json.Unmarshal(body, resterr)
		if err == nil {
			if resterr.Code == 0 {
				resterr.Code = res.StatusCode
			}
			return resterr
		}
	}
	return &ResponseError{
		Code:   res.StatusCode,
		Header: res.Header,
		Body:   string(body),
	}
}

// CheckResponse returns an error (of type *ResponseError) if the response
// status code is not 2xx. Unlike CheckJSONResponse it does not assume the body
// is a JSON error document.
//
// It gracefully degrades by ignoring errors while attemping to read the body.
// It is the caller's responsibility to close res.Body.
func CheckResponse(res *http.Response) error {
	if res.StatusCode >= 200 && res.StatusCode <= 299 {
		return nil
	}
	body, _ := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20)) // 1MiB
	return &ResponseError{
		Code:   res.StatusCode,
		Header: res.Header,
		Body:   string(body),
	}
}

// ResponseError contains an error response from the server.
type ResponseError struct {
	// Code is the HTTP response status code and will always be populated.
	Code int `json:"code"`
	// Header contains the response header fields from the server.
	Header http.Header
	// Body is the raw response returned by the server.
	// It is often but not always JSON, depending on how the request fails.
	Body string
}

func (e *ResponseError) Error() string {
	return fmt.Sprintf("response error %d", e.Code)
}