File size: 4,024 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 | package models
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5/middleware"
)
// ProblemContentType is the default content type for a Problem response
const ProblemContentType = "application/problem+json"
// ProblemType contains a URI that identifies the problem type. This URI will,
// ideally, contain human-readable documentation for the problem when
// de-referenced.
type ProblemType string
const (
// ProblemTypeDefault is the default problem type.
ProblemTypeDefault = ProblemType("about:blank")
// TODO: add more problem types that can be used to link to docs.
// ...
)
// Problem is the RFC 7807 response body.
type Problem interface {
Respond(w http.ResponseWriter)
Error() string
ProblemType() ProblemType
ProblemTitle() string
ProblemStatus() int
}
// StatusProblem is the RFC 7807 response body without additional fields.
type StatusProblem struct {
Err error `json:"-"` // low-level runtime error
// Type is a URI reference that identifies the problem type.
Type ProblemType `json:"type"`
// Title is a short, human-readable summary of the problem type.
Title string `json:"title"`
// Status is the HTTP status code generated by the origin server for this occurrence of the problem.
Status int `json:"status"`
// Detail is a human-readable explanation specific to this occurrence of the problem.
Detail string `json:"detail,omitempty"`
// Instance is a URI reference that identifies the specific occurrence of the problem.
Instance string `json:"instance,omitempty"`
// Extension fields are allowed by spec, see https://datatracker.ietf.org/doc/html/rfc7807#section-3.2
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
var _ Problem = (*StatusProblem)(nil)
func (p *StatusProblem) Error() string {
if p.Err == nil {
return fmt.Sprintf("[%s] %s", p.Title, p.Detail)
}
return fmt.Sprintf("[%s] %s - %s", p.Title, p.Err.Error(), p.Detail)
}
func (p *StatusProblem) RawError() error {
return p.Err
}
func (p *StatusProblem) ProblemType() ProblemType {
return p.Type
}
func (p *StatusProblem) ProblemStatus() int {
return p.Status
}
func (p *StatusProblem) ProblemTitle() string {
return p.Title
}
// Respond will render the problem as JSON to the provided ResponseWriter.
func (p *StatusProblem) Respond(w http.ResponseWriter) {
RespondProblem(p, w)
}
// Respond will render the problem as JSON to the provided ResponseWriter.
func RespondProblem(problem Problem, w http.ResponseWriter) {
// Respond
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
_ = enc.Encode(problem)
w.Header().Set("Content-Type", ProblemContentType)
w.WriteHeader(problem.ProblemStatus())
_, _ = w.Write(buf.Bytes()) // nosemgrep
}
// NewStatusProblem will generate a problem for the provided HTTP status
// code. The Problem's Status field will be set to match the status argument,
// and the Title will be set to the default Go status text for that code.
func NewStatusProblem(ctx context.Context, err error, status int) *StatusProblem {
var instance string
reqID := middleware.GetReqID(ctx)
if reqID != "" {
instance = fmt.Sprintf("urn:request:%s", reqID)
}
// Set context canceled errors to 408.
// Context canceled errors either happen when the client cancels the request or when a timeout happens in dependency.
// If client cancels the request, the status code doesn't matter, because the client will not see the response.
// If timeout happens in dependency, we want to return 408 to the client.
if err != nil && strings.Contains(err.Error(), "context canceled") {
status = http.StatusRequestTimeout
}
var detail string
if err != nil {
detail = err.Error()
}
if status == http.StatusInternalServerError {
detail = ""
}
return &StatusProblem{
Err: err,
Type: ProblemTypeDefault,
Title: http.StatusText(status),
Status: status,
Detail: detail,
Instance: instance,
Extensions: map[string]interface{}{},
}
}
|