-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresponse.go
More file actions
107 lines (99 loc) · 3.46 KB
/
Copy pathresponse.go
File metadata and controls
107 lines (99 loc) · 3.46 KB
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
package servekit
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
)
// ResponseEncoder writes successful responses for Handle.
//
// Returning an error passes control to the server ErrorEncoder. Implementations
// should avoid committing a success status or body before returning an error
// whenever practical. Once a success response has been committed, the
// ErrorEncoder may no longer be able to replace it with an error response.
type ResponseEncoder func(http.ResponseWriter, *http.Request, any) error
// ErrorEncoder writes error responses for Handle.
//
// The returned error is ignored by Servekit, so implementations should treat
// best-effort response writes as terminal. If the response has already been
// committed by an earlier writer, an ErrorEncoder may not be able to change the
// status code or replace the response body.
type ErrorEncoder func(http.ResponseWriter, *http.Request, error) error
// JSONResponse returns the default success encoder for Handle.
//
// A nil payload writes HTTP 204 No Content with no body. A non-nil payload
// writes HTTP 200 with Content-Type application/json and body shape
// {"data": <payload>}. JSONResponse serializes the complete response before
// committing HTTP 200 so Handle can delegate serialization failures to the
// server ErrorEncoder.
func JSONResponse() ResponseEncoder {
return func(w http.ResponseWriter, _ *http.Request, payload any) error {
if payload == nil {
w.WriteHeader(http.StatusNoContent)
return nil
}
body, err := json.Marshal(map[string]any{"data": payload})
if err != nil {
return err
}
body = append(body, '\n')
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(body)
return err
}
}
// JSONError returns the default error encoder for Handle.
//
// JSONError maps HTTPError values and pointers to their usable final StatusCode,
// maps context cancellation and deadline errors to HTTP 504, and otherwise
// returns HTTP 500. The payload shape is {"error": "..."} and includes
// request_id when one is present in the request context.
func JSONError() ErrorEncoder {
return func(w http.ResponseWriter, r *http.Request, err error) error {
status := statusFromError(err)
return writeDefaultJSONError(w, status, clientErrorMessage(err, status), RequestIDFromContext(r.Context()))
}
}
func clientErrorMessage(err error, status int) string {
if httpErr, ok := asHTTPError(err); ok {
if httpErr == nil {
return defaultClientErrorMessage(status)
}
if httpErr.Message != "" {
return httpErr.Message
}
if httpErr.StatusCode > 0 && !validHTTPErrorStatus(httpErr.StatusCode) {
return defaultClientErrorMessage(status)
}
}
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return "request body too large"
}
switch {
case errors.Is(err, context.DeadlineExceeded):
return "request timed out"
case errors.Is(err, context.Canceled):
return "request canceled"
}
return defaultClientErrorMessage(status)
}
func defaultClientErrorMessage(status int) string {
if text := http.StatusText(status); text != "" {
return strings.ToLower(text)
}
return "error"
}
func writeDefaultJSONError(w http.ResponseWriter, status int, message, requestID string) error {
body := map[string]any{"error": message}
if requestID != "" {
body["request_id"] = requestID
}
h := w.Header()
h.Del("Content-Length")
h.Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(body)
}