-
Notifications
You must be signed in to change notification settings - Fork 15
/
error.go
170 lines (149 loc) · 5.42 KB
/
error.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// Copyright 2021-2024 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause
package restful
import (
"encoding/json"
"errors"
"net/http"
"strings"
)
const maxErrBodyLen = 4096
var (
// ErrNonHTTPSURL means that using non-https URL not allowed.
ErrNonHTTPSURL = errors.New("non-https URL not allowed")
// ErrUnexpectedContentType is returned if content-type is unexpected.
// It may be wrapped, so use errors.Is() for checking.
ErrUnexpectedContentType = errors.New("unexpected Content-Type")
)
type restError struct {
err error
statusCode int
problemDetails ProblemDetails
contentType string
body []byte
}
// InvalidParam is the common InvalidParam object defined in 3GPP TS 29.571
type InvalidParam struct {
Param string `json:"param"`
Reason string `json:"reason,omitempty"`
}
// ProblemDetails is a structure defining fields for RFC 7807 error responses.
type ProblemDetails struct {
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`
Detail string `json:"detail,omitempty"`
Cause string `json:"cause,omitempty"`
Instance string `json:"instance,omitempty"`
Status int `json:"status,omitempty"`
InvalidParams []InvalidParam `json:"invalidParams,omitempty"`
}
// String makes string of ProblemDetails.
func (e ProblemDetails) String() string {
b, _ := json.Marshal(e)
return string(b)
}
// ProblemDetails adds ProblemDetails data to error.
func (e *restError) ProblemDetails(pd ProblemDetails) error {
e.problemDetails = pd
return e
}
// Error returns error string.
func (e restError) Error() string {
errStr := ""
if e.err != nil {
errStr = e.err.Error()
}
if e.problemDetails.Detail == "" {
return errStr
}
if errStr == "" {
return e.problemDetails.Detail
}
return e.problemDetails.Detail + ": " + errStr
}
// Unwrap returns wrapped error.
// https://blog.golang.org/go1.13-errors
func (e *restError) Unwrap() error {
return e.err
}
// NewError creates a new error that contains HTTP status code.
// Coupling HTTP status code to error makes functions return values clean. And controls what Lambda sends on errors.
//
// if err != nil {return restful.NewError(err, http.StatusBadRequest)}
//
// Parameter description is optional, caller may provide extra description, appearing at the beginning of the error string.
//
// if err != nil {return restful.NewError(err, http.StatusBadRequest, "bad data")}
//
// Parameter err may be nil, if there is no error to wrap or original error text is better not to be propagated.
//
// if err != nil {return restful.NewError(nil, http.StatusBadRequest, "bad data")}
func NewError(err error, statusCode int, description ...string) error {
return &restError{err: err, statusCode: statusCode, problemDetails: ProblemDetails{Detail: strings.Join(description, " ")}}
}
// NewErrorWithBody creates new error with custom HTTP status code, content-type and payload body.
func NewErrorWithBody(err error, statusCode int, contentType string, body []byte) error {
if len(body) > maxErrBodyLen {
return &restError{err: err, statusCode: statusCode}
}
return &restError{err: err, statusCode: statusCode, contentType: contentType, body: body}
}
// NewDetailedError creates a new error with specified problem details JSON structure (RFC7807)
func NewDetailedError(err error, status int, pd ProblemDetails) error {
return &restError{err: err, statusCode: status, problemDetails: pd}
}
// DetailError adds further description to the error. Useful when cascading return values.
// Can be used on any error, though mostly used on errors created by restful.NewError() / NewDetailedError()
// E.g. restful.DetailError(err, "db query failed")
func DetailError(err error, description string) error {
return &restError{err: err, statusCode: GetErrStatusCodeElse(err, 0), problemDetails: ProblemDetails{Detail: description}}
}
// GetErrStatusCode returns status code of error response.
// If err is nil then http.StatusOK returned.
// If no status stored (e.g. unexpected content-type received) then http.StatusInternalServerError returned.
func GetErrStatusCode(err error) int {
status := GetErrStatusCodeElse(err, -1)
if status <= 0 {
return http.StatusInternalServerError
}
return status
}
// GetErrStatusCodeElse returns status code of error response, if available.
// Else retuns the one the caller provided. Probably transport error happened and no HTTP response was received.
// If err is nil then http.StatusOK returned.
func GetErrStatusCodeElse(err error, elseStatusCode int) int {
if err != nil {
var e *restError
if errors.As(err, &e) {
return e.statusCode
}
return elseStatusCode
}
return http.StatusOK
}
// IsConnectError determines if error is due to failed connection.
// I.e. does not contain HTTP status code, or 502 / 503 / 504.
func IsConnectError(err error) bool {
status := GetErrStatusCodeElse(err, 502)
return status >= 502 && status <= 504
}
// GetErrBody returns unprocessed body of a response with error status.
//
// err := restful.Get()
// contentType, Body := restful.GetErrBody(err)
// if body != nil {fmt.Print(string(body))}
func GetErrBody(err error) (string, []byte) {
if err != nil {
var e *restError
if errors.As(err, &e) {
if e.body != nil {
return e.contentType, e.body
}
if e.problemDetails.Detail != "" {
return ContentTypeProblemJSON, []byte(e.problemDetails.Detail)
}
}
}
return "", nil
}