File size: 5,534 Bytes
fea99b3 | 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | package commonhttp
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"mime"
"net/http"
"net/url"
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport/encoder"
"github.com/openmeterio/openmeter/pkg/models"
)
// GetMediaType returns the media type of the request.
// If the media type is invalid, it defaults to JSON.
func GetMediaType(r *http.Request) (string, error) {
var err error
// Parse media type
accept := r.Header.Get("Accept")
if accept == "" {
accept = "application/json"
}
mediatype, _, err := mime.ParseMediaType(accept)
// Browser can send back media type Go marks as invalid
// If that happens, default to JSON
if err != nil {
err = fmt.Errorf("invalid media type, default to json: %w", err)
mediatype = "application/json"
}
return mediatype, err
}
// JSONResponseEncoder encodes a response as JSON.
func JSONResponseEncoder[Response any](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
return jsonResponseEncoder(w, http.StatusOK, response)
}
// JSONResponseEncoder encodes a response as JSON.
func jsonResponseEncoder[Response any](w http.ResponseWriter, statusCode int, response Response) error {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
if err := enc.Encode(response); err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_, err := w.Write(buf.Bytes())
if err != nil {
return err
}
return nil
}
func JSONResponseEncoderWithStatus[Response any](statusCode int) encoder.ResponseEncoder[Response] {
return func(ctx context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
return jsonResponseEncoder(w, statusCode, response)
}
}
// PlainTextResponseEncoder encodes a response as PlainText.
func PlainTextResponseEncoder[Response string](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
return plainTextResponseEncoder(w, http.StatusOK, response)
}
// PlainTextResponseEncoder encodes a response as PlainText.
func plainTextResponseEncoder[Response string](w http.ResponseWriter, statusCode int, response Response) error {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(statusCode)
_, err := w.Write([]byte(response))
if err != nil {
return err
}
return nil
}
// CSVResponse is a response that can be encoded as CSV.
type CSVResponse interface {
FileName() string
Records() [][]string
}
// CSVResponseEncoder encodes a response as CSV.
func CSVResponseEncoder[Response CSVResponse](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
return csvResponseEncoder(w, http.StatusOK, response)
}
// CSVResponseEncoder encodes a response as CSV.
func csvResponseEncoder[Response CSVResponse](w http.ResponseWriter, statusCode int, response Response) error {
buf := &bytes.Buffer{}
writer := csv.NewWriter(buf)
if err := writer.WriteAll(response.Records()); err != nil {
return fmt.Errorf("writing record to csv: %w", err)
}
if err := writer.Error(); err != nil {
return fmt.Errorf("writing csv: %w", err)
}
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", response.FileName()))
w.WriteHeader(statusCode)
if _, err := w.Write(buf.Bytes()); err != nil {
return err
}
return nil
}
func EmptyResponseEncoder[Response any](statusCode int) encoder.ResponseEncoder[Response] {
return func(_ context.Context, w http.ResponseWriter, _ *http.Request, resp Response) error {
w.WriteHeader(statusCode)
return nil
}
}
// DummyErrorEncoder is a dummy error encoder that always returns a 400 status code with the received error.
func DummyErrorEncoder() encoder.ErrorEncoder {
return func(ctx context.Context, err error, w http.ResponseWriter, _ *http.Request) bool {
NewHTTPError(http.StatusBadRequest, err).EncodeError(ctx, w)
return true
}
}
// GenericErrorEncoder is an error encoder that encodes the error as a generic error.
func GenericErrorEncoder() encoder.ErrorEncoder {
return func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) bool {
return HandleIssueIfHTTPStatusKnown(ctx, err, w) ||
HandleErrorIfTypeMatches[*models.GenericConflictError](ctx, http.StatusConflict, err, w) ||
HandleErrorIfTypeMatches[*models.GenericForbiddenError](ctx, http.StatusForbidden, err, w) ||
HandleErrorIfTypeMatches[*models.GenericNotImplementedError](ctx, http.StatusNotImplemented, err, w) ||
HandleErrorIfTypeMatches[*models.GenericValidationError](ctx, http.StatusBadRequest, err, w) ||
HandleErrorIfTypeMatches[*models.GenericNotFoundError](ctx, http.StatusNotFound, err, w) ||
HandleErrorIfTypeMatches[*models.GenericUnauthorizedError](ctx, http.StatusUnauthorized, err, w) ||
HandleErrorIfTypeMatches[*models.GenericPreConditionFailedError](ctx, http.StatusPreconditionFailed, err, w)
}
}
func RedirectResponseEncoder[Response string](statusCode int) encoder.ResponseEncoder[Response] {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, response Response) error {
redirectURL := string(response)
_, err := url.Parse(redirectURL)
if err != nil {
return fmt.Errorf("invalid redirect url: %w", err)
}
if statusCode < 300 || statusCode > 399 {
return fmt.Errorf("invalid redirect status code: it must be in 3xx range: %w", err)
}
http.Redirect(w, r, redirectURL, statusCode)
return nil
}
}
|