forked from stellar/go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
151 lines (128 loc) · 4.37 KB
/
main.go
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
package problem
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/support/log"
)
// P is a struct that represents an error response to be rendered to a connected
// client.
type P struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Extras map[string]interface{} `json:"extras,omitempty"`
}
func (p P) Error() string {
return fmt.Sprintf("problem: %s", p.Type)
}
var errToProblemMap = map[error]P{}
// RegisterError records an error -> P mapping, allowing the app to register
// specific errors that may occur in other packages to be rendered as a specific
// P instance.
//
// For example, you might want to render any sql.ErrNoRows errors as a
// problem.NotFound, and you would do so by calling:
//
// problem.RegisterError(sql.ErrNoRows, problem.NotFound) in you application
// initialization sequence
func RegisterError(err error, p P) {
errToProblemMap[err] = p
}
// Inflate sets some basic parameters on the problem, mostly the type for now
func Inflate(p *P) {
//TODO: add requesting url to extra info
//TODO: make this prefix configurable
p.Type = "https://stellar.org/horizon-errors/" + p.Type
p.Instance = ""
}
// HasProblem types can be transformed into a problem.
// Implement it for custom errors.
type HasProblem interface {
Problem() P
}
// Render writes a http response to `w`, compliant with the "Problem
// Details for HTTP APIs" RFC:
// https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
//
// `p` is the problem, which may be either a concrete P struct, an implementor
// of the `HasProblem` interface, or an error. Any other value for `p` will
// panic.
func Render(ctx context.Context, w http.ResponseWriter, p interface{}) {
switch p := p.(type) {
case P:
render(ctx, w, p)
case *P:
render(ctx, w, *p)
case HasProblem:
render(ctx, w, p.Problem())
case error:
renderErr(ctx, w, p)
default:
panic(fmt.Sprintf("Invalid problem: %v+", p))
}
}
func render(ctx context.Context, w http.ResponseWriter, p P) {
Inflate(&p)
w.Header().Set("Content-Type", "application/problem+json; charset=utf-8")
js, err := json.MarshalIndent(p, "", " ")
if err != nil {
err := errors.Wrap(err, "failed to encode problem")
log.Ctx(ctx).WithStack(err).Error(err)
http.Error(w, "error rendering problem", http.StatusInternalServerError)
return
}
w.WriteHeader(p.Status)
w.Write(js)
}
func renderErr(ctx context.Context, w http.ResponseWriter, err error) {
origErr := errors.Cause(err)
p, ok := errToProblemMap[origErr]
// If this error is not a registered error
// log it and replace it with a 500 error
if !ok {
log.Ctx(ctx).WithStack(err).Error(err)
p = ServerError
}
render(ctx, w, p)
}
// ServerError is a well-known problem type. Use it as a shortcut.
var ServerError = P{
Type: "server_error",
Title: "Internal Server Error",
Status: http.StatusInternalServerError,
Detail: "An error occurred while processing this request. This is usually due " +
"to a bug within the server software. Trying this request again may " +
"succeed if the bug is transient, otherwise please report this issue " +
"to the issue tracker at: https://github.com/stellar/go/services/horizon/internal/issues." +
" Please include this response in your issue.",
}
// NotFound is a well-known problem type. Use it as a shortcut in your actions
var NotFound = P{
Type: "not_found",
Title: "Resource Missing",
Status: http.StatusNotFound,
Detail: "The resource at the url requested was not found. This is usually " +
"occurs for one of two reasons: The url requested is not valid, or no " +
"data in our database could be found with the parameters provided.",
}
// BadRequest is a well-known problem type. Use it as a shortcut
// in your actions.
var BadRequest = P{
Type: "bad_request",
Title: "Bad Request",
Status: http.StatusBadRequest,
Detail: "The request you sent was invalid in some way",
}
// MakeInvalidFieldProblem is a helper function to make a BadRequest with extras
func MakeInvalidFieldProblem(name string, reason error) *P {
br := BadRequest
br.Extras = map[string]interface{}{}
br.Extras["invalid_field"] = name
br.Extras["reason"] = reason.Error()
return &br
}