-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresponse.go
More file actions
165 lines (147 loc) · 5.5 KB
/
Copy pathresponse.go
File metadata and controls
165 lines (147 loc) · 5.5 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
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package httpserver
import (
"context"
"encoding/json"
"io"
"net/http"
"unsafe"
"github.com/rs/zerolog"
)
const (
marshallerIsDirect uint = iota
marshallerIsJson
)
// Response is a handle to response state owned by a [Context]. Copies share the
// same state. The zero value is invalid.
type Response struct{ ctx *Context }
// Status returns the configured HTTP status, or zero before
// [Context.NewResponse] is called.
func (r Response) Status() int { return r.ctx.status }
// Header returns the live response header map. A later
// [Context.NewResponse] call clears it.
func (r Response) Header() http.Header { return r.ctx.writer.Header() }
// Body returns the configured body value, or nil if no body is set.
func (r Response) Body() any { return r.ctx.body }
// Cookie appends a Set-Cookie header for cookie to the response.
func (r Response) Cookie(cookie http.Cookie) {
r.Header().Add("Set-Cookie", cookie.String())
}
// BytesBody sets a raw byte body without setting Content-Type.
func (r Response) BytesBody(body []byte) {
r.ctx.body, r.ctx.marshaller = body, marshallerIsDirect
}
// StringBody sets a raw string body without setting Content-Type.
func (r Response) StringBody(body string) {
r.ctx.body, r.ctx.marshaller = body, marshallerIsDirect
}
// StreamBody sets a body writer without setting Content-Type. The HTTP status is
// committed before body runs, so an error returned by body can be logged but
// cannot change the response status.
func (r Response) StreamBody(body func(io.Writer) error) {
r.ctx.body, r.ctx.marshaller = body, marshallerIsDirect
}
// PlainTextBody sets body with Content-Type "text/plain; charset=utf-8".
func (r Response) PlainTextBody(body string) {
r.Header().Set("Content-Type", "text/plain; charset=utf-8")
r.ctx.body, r.ctx.marshaller = body, marshallerIsDirect
}
// OctetsBody sets body with Content-Type "application/octet-stream".
func (r Response) OctetsBody(body []byte) {
r.Header().Set("Content-Type", "application/octet-stream")
r.ctx.body, r.ctx.marshaller = body, marshallerIsDirect
}
// JsonBody stores body for JSON marshaling when the response is written.
// Successful marshaling sets Content-Type to "application/json; charset=utf-8".
// A marshal failure writes 500 Internal Server Error with an empty body.
func (r Response) JsonBody(body any) {
r.ctx.body, r.ctx.marshaller = body, marshallerIsJson
}
// MarshalZerologObject implements [zerolog.LogObjectMarshaler] for the
// configured status, headers, and body.
func (r Response) MarshalZerologObject(e *zerolog.Event) {
e.Int("status", r.ctx.status)
if header := r.ctx.writer.Header(); len(header) > 0 {
e.Any("header", header)
}
if r.ctx.body != nil {
e.Any("body", r.ctx.body)
}
}
// writeResponse commits the response currently stored in c to the underlying
// http.ResponseWriter. Router.Handle calls it once after the handler chain
// returns. JSON marshal failures and unsupported body types become empty 500
// responses because they are caught before any header is committed. Body write
// and stream errors occur after [http.ResponseWriter.WriteHeader]; the
// response status is already on the wire and cannot be replaced, so the error
// is logged and the connection is then aborted via panic(http.ErrAbortHandler),
// which server.ServeHTTP and net/http treat as a silent connection close.
func (c *Context) writeResponse(requestCtx context.Context) {
logger := zerolog.Ctx(requestCtx)
switch c.marshaller {
case marshallerIsJson:
data, err := json.Marshal(c.body)
if err != nil {
logger.Error().Err(err).Msg("Failed to marshal response as JSON")
clear(c.writer.Header())
c.writer.WriteHeader(http.StatusInternalServerError)
return
}
c.writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.writer.WriteHeader(c.status)
if count, err := c.writer.Write(data); err != nil {
logger.Error().Err(err).Int("count", count).Msg("Failed to write response")
break
}
return
default:
switch body := c.body.(type) {
case nil:
if c.status == 0 {
logger.Error().Msg("Response is missing")
clear(c.writer.Header())
c.writer.WriteHeader(http.StatusInternalServerError)
return
}
c.writer.WriteHeader(c.status)
return
case []byte:
c.writer.WriteHeader(c.status)
if count, err := c.writer.Write(body); err != nil {
logger.Error().Err(err).Int("count", count).Msg("Failed to write response body")
break
}
return
case string:
c.writer.WriteHeader(c.status)
if count, err := c.writer.Write(unsafeStringToBytes(body)); err != nil {
logger.Error().Err(err).Int("count", count).Msg("Failed to write response body")
break
}
return
case func(io.Writer) error:
c.writer.WriteHeader(c.status)
if err := body(c.writer); err != nil {
logger.Error().Err(err).Msg("Failed to write response body")
break
}
return
default:
logger.Error().Any("body", body).Msg("Unsupported response body type")
clear(c.writer.Header())
c.writer.WriteHeader(http.StatusInternalServerError)
return
}
}
panic(http.ErrAbortHandler)
}
// unsafeStringToBytes returns a zero-copy byte view of value for the immediate
// response write path. The returned slice aliases immutable string storage and
// must never be modified.
func unsafeStringToBytes(value string) []byte {
return unsafe.Slice(unsafe.StringData(value), len(value))
}