-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresponse.go
More file actions
358 lines (321 loc) · 10.7 KB
/
Copy pathresponse.go
File metadata and controls
358 lines (321 loc) · 10.7 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
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package api
import (
"context"
"io"
"net/http"
"reflect"
"strconv"
"time"
)
// Resp is a body-only response type parameterized by the body value.
// Use it when a handler needs to return just a body and none of the
// declarative metadata (status override, response headers, cookies).
// The T type parameter drives emission the same way a Body field's
// type does on a hand-written response:
//
// func(...) (*api.Resp[User], error) // JSON/XML body
// func(...) (*api.Resp[io.Reader], error) // streamed body
// func(...) (*api.Resp[<-chan api.Event], error) // SSE body
//
// For responses that also carry status, headers, or cookies, declare
// a custom response struct with tagged fields plus a Body field.
type Resp[T any] struct {
Body T
}
// encodeResponse writes a non-error handler response to w using the
// route's precomputed descriptor. It applies cookies, headers, resolves
// status, and dispatches the body by kind.
func encodeResponse(
w http.ResponseWriter,
r *http.Request,
resp any,
desc *responseDescriptor,
defaultStatus int,
codecs *codecRegistry,
) {
rv := reflect.ValueOf(resp)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
status := defaultStatus
if desc.status != nil {
if s := intFieldValue(rv.FieldByIndex(desc.status.index)); s != 0 {
status = s
}
}
for _, ck := range desc.cookies {
fv := rv.FieldByIndex(ck.index)
c, ok := fv.Interface().(Cookie)
if !ok || c.IsZero() {
continue
}
http.SetCookie(w, c.ToHTTPCookie(ck.name))
}
for _, h := range desc.headers {
fv := rv.FieldByIndex(h.index)
values := headerFieldValues(fv)
for _, v := range values {
if v == "" {
continue
}
w.Header().Add(h.name, v)
}
}
// Announce trailers up-front so the stdlib emits them after the body.
for _, tr := range desc.trailers {
w.Header().Add("Trailer", tr.name)
}
if isNoBodyStatus(status) || desc.body == nil {
w.WriteHeader(status)
writeTrailers(w, rv, desc.trailers)
return
}
bv := rv.FieldByIndex(desc.body.index)
switch desc.body.kind {
case bodyKindCodec:
writeCodecBody(w, r, bv, status, codecs)
case bodyKindReader:
writeReaderBody(w, r, bv, status)
case bodyKindChan:
writeChanBody(r.Context(), w, bv, status)
}
writeTrailers(w, rv, desc.trailers)
}
// writeTrailers emits announced trailer headers after the body has been
// written. Per net/http, trailer headers are set on w.Header() with the
// "Trailer:" prefix; the stdlib transport detects and emits them.
func writeTrailers(w http.ResponseWriter, rv reflect.Value, trailers []responseTrailerDesc) {
for _, tr := range trailers {
fv := rv.FieldByIndex(tr.index)
values := headerFieldValues(fv)
for _, v := range values {
if v == "" {
continue
}
w.Header().Add(http.TrailerPrefix+tr.name, v)
}
}
}
// isNoBodyStatus reports whether the HTTP status requires an empty body
// per RFC 9110 §6.4.1 (1xx informational, 204 No Content, 304 Not Modified).
func isNoBodyStatus(status int) bool {
return (status >= 100 && status < 200) || status == http.StatusNoContent || status == http.StatusNotModified
}
// writeCodecBody encodes a value via the negotiated response codec.
func writeCodecBody(w http.ResponseWriter, r *http.Request, bv reflect.Value, status int, codecs *codecRegistry) {
enc, _ := codecs.negotiate(r.Header.Get("Accept"))
w.Header().Set("Content-Type", enc.ContentType())
w.WriteHeader(status)
//nolint:errcheck,gosec // best-effort after WriteHeader
enc.Encode(w, bv.Interface())
}
// writeReaderBody copies bytes from an io.Reader body to w. If the reader
// also implements io.Seeker, the body is served via http.ServeContent so the
// client can request byte ranges (Range header) and conditional responses
// (If-Modified-Since / If-None-Match) work as defined by RFC 9110.
//
// When ServeContent is used, the response status is owned by ServeContent
// (200 OK or 206 Partial Content) — a Status field on the response struct is
// ignored for ReadSeeker bodies.
func writeReaderBody(w http.ResponseWriter, r *http.Request, bv reflect.Value, status int) {
if bv.IsNil() {
w.WriteHeader(status)
return
}
reader := bv.Interface().(io.Reader) //nolint:errcheck,forcetypeassert // descriptor guarantees io.Reader
if rs, ok := reader.(io.ReadSeeker); ok {
http.ServeContent(w, r, "", time.Time{}, rs)
return
}
w.WriteHeader(status)
//nolint:errcheck,gosec // best-effort streaming copy
io.Copy(w, reader)
}
// writeChanBody consumes events from a channel and emits them as SSE. It
// exits when the channel closes or the request context is cancelled.
func writeChanBody(ctx context.Context, w http.ResponseWriter, bv reflect.Value, status int) {
if bv.IsNil() {
w.WriteHeader(status)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(status)
flusher, _ := w.(http.Flusher) //nolint:errcheck // ok being false means no flushing
for {
chosen, recv, ok := reflect.Select([]reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: bv},
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())},
})
if chosen == 1 || !ok {
return
}
ev := recv.Interface().(Event) //nolint:errcheck,forcetypeassert // descriptor guarantees chan Event
//nolint:errcheck,gosec // best-effort SSE write
writeEvent(w, ev)
if flusher != nil {
flusher.Flush()
}
}
}
// intFieldValue extracts an int from a field that may be any signed or
// unsigned integer kind. Returns 0 for non-integer fields.
func intFieldValue(fv reflect.Value) int {
//exhaustive:ignore
switch fv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return int(fv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return int(fv.Uint())
default:
return 0
}
}
// headerFieldValues converts a field value to one or more strings suitable
// for a response header. Supports string, []string, integer kinds, and
// time.Time (RFC 1123 format).
func headerFieldValues(fv reflect.Value) []string {
//exhaustive:ignore
switch fv.Kind() {
case reflect.String:
return []string{fv.String()}
case reflect.Slice:
if fv.Type().Elem().Kind() == reflect.String {
out := make([]string, fv.Len())
for i := range fv.Len() {
out[i] = fv.Index(i).String()
}
return out
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return []string{strconv.FormatInt(fv.Int(), 10)}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return []string{strconv.FormatUint(fv.Uint(), 10)}
case reflect.Struct:
if t, ok := fv.Interface().(time.Time); ok {
if t.IsZero() {
return nil
}
return []string{t.UTC().Format(http.TimeFormat)}
}
}
return nil
}
// mergeErr combines a route's scope-level error template with the
// inline state carried by the handler-returned *Err. Scalar options
// (message, body mapper, cause) from the inline state replace the
// template; map options (headers, cookies keyed by name) replace
// entries with the same key; list options (details) accumulate with
// template entries first.
func mergeErr(template, inline *Err) *Err {
final := &Err{code: inline.code}
if inline.message != "" {
final.message = inline.message
} else if template != nil {
final.message = template.message
}
if inline.body != nil {
final.body = inline.body
} else if template != nil {
final.body = template.body
}
if inline.cause != nil {
final.cause = inline.cause
} else if template != nil {
final.cause = template.cause
}
if template != nil {
for name, values := range template.headers {
if final.headers == nil {
final.headers = http.Header{}
}
final.headers[name] = append([]string{}, values...)
}
for name, c := range template.cookies {
if final.cookies == nil {
final.cookies = make(map[string]Cookie, len(template.cookies))
}
final.cookies[name] = c
}
final.details = append(final.details, template.details...)
}
for name, values := range inline.headers {
if final.headers == nil {
final.headers = http.Header{}
}
final.headers[name] = append([]string{}, values...)
}
for name, c := range inline.cookies {
if final.cookies == nil {
final.cookies = make(map[string]Cookie)
}
final.cookies[name] = c
}
final.details = append(final.details, inline.details...)
return final
}
// errInfoView wraps *Err with the request URI so body mappers can read
// ErrorInfo.Instance() without the *Err itself knowing about HTTP. The
// framework constructs this view at emission time.
//
//nolint:errname // internal view type, not a distinct error.
type errInfoView struct {
*Err
instance string
}
func (v *errInfoView) Instance() string { return v.instance }
// emitErr renders a fully-resolved *Err to the response writer. Status
// comes from the Code. Cookies and headers are written first, then body
// (if any) is emitted via the configured mapper.
func emitErr(w http.ResponseWriter, r *http.Request, e *Err, codecs *codecRegistry) {
status := e.code.HTTPStatus()
for name, c := range e.cookies {
http.SetCookie(w, c.ToHTTPCookie(name))
}
for name, values := range e.headers {
for _, v := range values {
w.Header().Add(name, v)
}
}
// HTTP: 1xx, 204, 304 carry no body.
if isNoBodyStatus(status) || e.body == nil {
w.WriteHeader(status)
return
}
info := &errInfoView{Err: e, instance: r.URL.RequestURI()}
rv, skip := e.body.produce(r.Context(), info)
if skip {
w.WriteHeader(status)
return
}
// Dispatch by the mapper's element type. *string → text/plain;
// everything else → negotiated codec.
if e.body.elemType().Kind() == reflect.String {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(status)
s, _ := rv.Elem().Interface().(string) //nolint:errcheck // kind guarantees string
//nolint:errcheck,gosec // best-effort after WriteHeader
io.WriteString(w, s)
return
}
bodyVal := rv.Interface()
// Error bodies always emit regardless of whether the request's
// Accept header matched a codec — clients benefit from seeing the
// error even when their Accept is too narrow. Negotiate first, but
// fall back to the router's default codec if negotiation failed.
enc, ok := codecs.negotiate(r.Header.Get("Accept"))
if !ok || enc == nil {
enc = codecs.defaultEncoder()
}
contentType := enc.ContentType()
// If the body value declares its own content type (e.g. ProblemDetails
// emits application/problem+json per RFC 9457), honor it.
if ct, ok := bodyVal.(interface{ ContentType() string }); ok {
contentType = ct.ContentType()
}
w.Header().Set("Content-Type", contentType)
w.WriteHeader(status)
//nolint:errcheck,gosec // best-effort after WriteHeader
enc.Encode(w, bodyVal)
}