problem.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. package context
  2. import (
  3. "encoding/xml"
  4. "fmt"
  5. "math"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. )
  11. // Problem Details for HTTP APIs.
  12. // Pass a Problem value to `context.Problem` to
  13. // write an "application/problem+json" response.
  14. //
  15. // Read more at: https://github.com/kataras/iris/blob/main/_examples/routing/http-errors.
  16. type Problem map[string]interface{}
  17. // NewProblem retruns a new Problem.
  18. // Head over to the `Problem` type godoc for more.
  19. func NewProblem() Problem {
  20. p := make(Problem)
  21. return p
  22. }
  23. func (p Problem) keyExists(key string) bool {
  24. if p == nil {
  25. return false
  26. }
  27. _, found := p[key]
  28. return found
  29. }
  30. // DefaultProblemStatusCode is being sent to the client
  31. // when Problem's status is not a valid one.
  32. var DefaultProblemStatusCode = http.StatusBadRequest
  33. func (p Problem) getStatus() (int, bool) {
  34. statusField, found := p["status"]
  35. if !found {
  36. return DefaultProblemStatusCode, false
  37. }
  38. status, ok := statusField.(int)
  39. if !ok {
  40. return DefaultProblemStatusCode, false
  41. }
  42. if !StatusCodeNotSuccessful(status) {
  43. return DefaultProblemStatusCode, false
  44. }
  45. return status, true
  46. }
  47. func isEmptyTypeURI(uri string) bool {
  48. return uri == "" || uri == "about:blank"
  49. }
  50. func (p Problem) getURI(key string) string {
  51. f, found := p[key]
  52. if found {
  53. if typ, ok := f.(string); ok {
  54. if !isEmptyTypeURI(typ) {
  55. return typ
  56. }
  57. }
  58. }
  59. return ""
  60. }
  61. // Updates "type" field to absolute URI, recursively.
  62. func (p Problem) updateURIsToAbs(ctx *Context) {
  63. if p == nil {
  64. return
  65. }
  66. if uriRef := p.getURI("type"); uriRef != "" && !strings.HasPrefix(uriRef, "http") {
  67. p.Type(ctx.AbsoluteURI(uriRef))
  68. }
  69. if uriRef := p.getURI("instance"); uriRef != "" {
  70. p.Instance(ctx.AbsoluteURI(uriRef))
  71. }
  72. if cause, ok := p["cause"]; ok {
  73. if causeP, ok := cause.(Problem); ok {
  74. causeP.updateURIsToAbs(ctx)
  75. }
  76. }
  77. }
  78. const (
  79. problemTempKeyPrefix = "@temp_"
  80. )
  81. // TempKey sets a temporary key-value pair, which is being removed
  82. // on the its first get.
  83. func (p Problem) TempKey(key string, value interface{}) Problem {
  84. return p.Key(problemTempKeyPrefix+key, value)
  85. }
  86. // GetTempKey returns the temp value based on "key" and removes it.
  87. func (p Problem) GetTempKey(key string) interface{} {
  88. key = problemTempKeyPrefix + key
  89. v, ok := p[key]
  90. if ok {
  91. delete(p, key)
  92. return v
  93. }
  94. return nil
  95. }
  96. // Key sets a custom key-value pair.
  97. func (p Problem) Key(key string, value interface{}) Problem {
  98. p[key] = value
  99. return p
  100. }
  101. // Type URI SHOULD resolve to HTML [W3C.REC-html5-20141028]
  102. // documentation that explains how to resolve the problem.
  103. // Example: "https://example.net/validation-error"
  104. //
  105. // Empty URI or "about:blank", when used as a problem type,
  106. // indicates that the problem has no additional semantics beyond that of the HTTP status code.
  107. // When "about:blank" is used and "title" was not set-ed,
  108. // the title is being automatically set the same as the recommended HTTP status phrase for that code
  109. // (e.g., "Not Found" for 404, and so on) on `Status` call.
  110. //
  111. // Relative paths are also valid when writing this Problem to an Iris Context.
  112. func (p Problem) Type(uri string) Problem {
  113. return p.Key("type", uri)
  114. }
  115. // Title sets the problem's title field.
  116. // Example: "Your request parameters didn't validate."
  117. // It is set to status Code text if missing,
  118. // (e.g., "Not Found" for 404, and so on).
  119. func (p Problem) Title(title string) Problem {
  120. return p.Key("title", title)
  121. }
  122. // Status sets HTTP error code for problem's status field.
  123. // Example: 404
  124. //
  125. // It is required.
  126. func (p Problem) Status(statusCode int) Problem {
  127. shouldOverrideTitle := !p.keyExists("title")
  128. // if !shouldOverrideTitle {
  129. // typ, found := p["type"]
  130. // shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string))
  131. // }
  132. if shouldOverrideTitle {
  133. // Set title by code.
  134. p.Title(http.StatusText(statusCode))
  135. }
  136. return p.Key("status", statusCode)
  137. }
  138. // Detail sets the problem's detail field.
  139. // Example: "Optional details about the error...".
  140. func (p Problem) Detail(detail string) Problem {
  141. return p.Key("detail", detail)
  142. }
  143. // DetailErr calls `Detail(err.Error())`.
  144. func (p Problem) DetailErr(err error) Problem {
  145. if err == nil {
  146. return p
  147. }
  148. return p.Key("detail", err.Error())
  149. }
  150. // Instance sets the problem's instance field.
  151. // A URI reference that identifies the specific
  152. // occurrence of the problem. It may or may not yield further
  153. // information if dereferenced.
  154. func (p Problem) Instance(instanceURI string) Problem {
  155. return p.Key("instance", instanceURI)
  156. }
  157. // Cause sets the problem's cause field.
  158. // Any chain of problems.
  159. func (p Problem) Cause(cause Problem) Problem {
  160. if !cause.Validate() {
  161. return p
  162. }
  163. return p.Key("cause", cause)
  164. }
  165. // Validate reports whether this Problem value is a valid problem one.
  166. func (p Problem) Validate() bool {
  167. // A nil problem is not a valid one.
  168. if p == nil {
  169. return false
  170. }
  171. return p.keyExists("type") &&
  172. p.keyExists("title") &&
  173. p.keyExists("status")
  174. }
  175. // Error method completes the go error.
  176. // Returns the "[Status] Title" string form of this Problem.
  177. // If Problem is not a valid one, it returns "invalid problem".
  178. func (p Problem) Error() string {
  179. if !p.Validate() {
  180. return "invalid problem"
  181. }
  182. return fmt.Sprintf("[%d] %s", p["status"], p["title"])
  183. }
  184. // MarshalXML makes this Problem XML-compatible content to render.
  185. func (p Problem) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
  186. if len(p) == 0 {
  187. return nil
  188. }
  189. err := e.EncodeToken(start)
  190. if err != nil {
  191. return err
  192. }
  193. // toTitle := cases.Title(language.English)
  194. // toTitle.String(k)
  195. for k, v := range p {
  196. // convert keys like "type" to "Type", "productName" to "ProductName" and e.t.c. when xml.
  197. err = e.Encode(xmlMapEntry{XMLName: xml.Name{Local: strings.Title(k)}, Value: v})
  198. if err != nil {
  199. return err
  200. }
  201. }
  202. return e.EncodeToken(start.End())
  203. }
  204. // DefaultProblemOptions the default options for `Context.Problem` method.
  205. var DefaultProblemOptions = ProblemOptions{
  206. JSON: JSON{Indent: " "},
  207. XML: XML{Indent: " "},
  208. }
  209. // ProblemOptions the optional settings when server replies with a Problem.
  210. // See `Context.Problem` method and `Problem` type for more details.
  211. type ProblemOptions struct {
  212. // JSON are the optional JSON renderer options.
  213. JSON JSON
  214. // RenderXML set to true if want to render as XML doc.
  215. // See `XML` option field too.
  216. RenderXML bool
  217. // XML are the optional XML renderer options.
  218. // Affect only when `RenderXML` field is set to true.
  219. XML XML
  220. // RetryAfter sets the Retry-After response header.
  221. // https://tools.ietf.org/html/rfc7231#section-7.1.3
  222. // The value can be one of those:
  223. // time.Time
  224. // time.Duration for seconds
  225. // int64, int, float64 for seconds
  226. // string for duration string or for datetime string.
  227. //
  228. // Examples:
  229. // time.Now().Add(5 * time.Minute),
  230. // 300 * time.Second,
  231. // "5m",
  232. // 300
  233. RetryAfter interface{}
  234. // A function that, if specified, can dynamically set
  235. // retry-after based on the request. Useful for ProblemOptions reusability.
  236. // Should return time.Time, time.Duration, int64, int, float64 or string.
  237. //
  238. // Overrides the RetryAfter field.
  239. RetryAfterFunc func(*Context) interface{}
  240. }
  241. func parseDurationToSeconds(dur time.Duration) int64 {
  242. return int64(math.Round(dur.Seconds()))
  243. }
  244. func (o *ProblemOptions) parseRetryAfter(value interface{}, timeLayout string) string {
  245. // https://tools.ietf.org/html/rfc7231#section-7.1.3
  246. // Retry-After = HTTP-date / delay-seconds
  247. switch v := value.(type) {
  248. case int64:
  249. return strconv.FormatInt(v, 10)
  250. case int:
  251. return o.parseRetryAfter(int64(v), timeLayout)
  252. case float64:
  253. return o.parseRetryAfter(int64(math.Round(v)), timeLayout)
  254. case time.Time:
  255. return v.Format(timeLayout)
  256. case time.Duration:
  257. return o.parseRetryAfter(parseDurationToSeconds(v), timeLayout)
  258. case string:
  259. dur, err := time.ParseDuration(v)
  260. if err != nil {
  261. t, err := time.Parse(timeLayout, v)
  262. if err != nil {
  263. return ""
  264. }
  265. return o.parseRetryAfter(t, timeLayout)
  266. }
  267. return o.parseRetryAfter(parseDurationToSeconds(dur), timeLayout)
  268. }
  269. return ""
  270. }
  271. // Apply accepts a Context and applies specific response-time options.
  272. func (o *ProblemOptions) Apply(ctx *Context) {
  273. retryAfterHeaderValue := ""
  274. timeLayout := ctx.Application().ConfigurationReadOnly().GetTimeFormat()
  275. if o.RetryAfterFunc != nil {
  276. retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfterFunc(ctx), timeLayout)
  277. } else if o.RetryAfter != nil {
  278. retryAfterHeaderValue = o.parseRetryAfter(o.RetryAfter, timeLayout)
  279. }
  280. if retryAfterHeaderValue != "" {
  281. ctx.Header("Retry-After", retryAfterHeaderValue)
  282. }
  283. }