Files
Dmitry VerkhoturovandGitHub 1f34984dab Bump go-pkgz/rest to v1.24.0 and opt in to wildcard origins with credentials (#2157)
rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

The example module is tidied in the same commit, as it reaches go-pkgz/rest
through the replace directive and its indirect graph would otherwise keep the
old pin and fail the readonly module check in CI.

The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
2026-08-19 00:33:13 -05:00

124 lines
3.6 KiB
Go

// Package rest provides common middlewares and helpers for rest services
package rest
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
// JSON is a map alias, just for convenience
type JSON map[string]any
// RenderJSON sends data as json
func RenderJSON(w http.ResponseWriter, data any) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
if err := enc.Encode(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(buf.Bytes())
}
// RenderJSONFromBytes sends binary data as json
func RenderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(data); err != nil {
return fmt.Errorf("failed to send response to %s: %w", r.RemoteAddr, err)
}
return nil
}
// RenderJSONWithHTML allows html tags and forces charset=utf-8
func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v any) error {
encodeJSONWithHTML := func(v any) ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, fmt.Errorf("json encoding failed: %w", err)
}
return buf.Bytes(), nil
}
data, err := encodeJSONWithHTML(v)
if err != nil {
return err
}
return RenderJSONFromBytes(w, r, data)
}
// renderJSONWithStatus sends data as json and enforces status code
func renderJSONWithStatus(w http.ResponseWriter, data any, code int) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
if err := enc.Encode(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write(buf.Bytes())
}
// ParseFromTo parses from and to query params of the request
func ParseFromTo(r *http.Request) (from, to time.Time, err error) {
parseTimeStamp := func(ts string) (time.Time, error) {
formats := []string{
"2006-01-02T15:04:05.000000000",
"2006-01-02T15:04:05",
"2006-01-02T15:04",
"20060102",
time.RFC3339,
time.RFC3339Nano,
}
for _, f := range formats {
if t, e := time.Parse(f, ts); e == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("can't parse date %q", ts)
}
if from, err = parseTimeStamp(r.URL.Query().Get("from")); err != nil {
return from, to, fmt.Errorf("incorrect from time: %w", err)
}
if to, err = parseTimeStamp(r.URL.Query().Get("to")); err != nil {
return from, to, fmt.Errorf("incorrect to time: %w", err)
}
return from, to, nil
}
// DecodeJSON decodes json request from http.Request to given type
func DecodeJSON[T any](r *http.Request, res *T) error {
if err := json.NewDecoder(r.Body).Decode(&res); err != nil {
return fmt.Errorf("decode json: %w", err)
}
return nil
}
// EncodeJSON encodes given type to http.ResponseWriter and sets status code and content type header.
// The value is encoded before anything is written, so an encoding failure leaves the response
// uncommitted and the caller is free to replace it with an error status. Write failures are reported
// as well, by which point the response has already been committed.
func EncodeJSON[T any](w http.ResponseWriter, status int, v T) error {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(v); err != nil {
return fmt.Errorf("encode json: %w", err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if _, err := w.Write(buf.Bytes()); err != nil {
return fmt.Errorf("write json: %w", err)
}
return nil
}