-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathctx.go
More file actions
346 lines (310 loc) · 10.3 KB
/
Copy pathctx.go
File metadata and controls
346 lines (310 loc) · 10.3 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
// Copyright 2015 Eryx <evorui at gmail dot com>, All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpsrv
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
)
// Ctx carries the request/response state for a Handler. It is implemented over
// plain net/http and built per request; path params come from the standard
// library (r.PathValue, set by the router), so Ctx.Params needs no custom
// context wiring.
type Ctx interface {
// Request / Response
Request() *http.Request
Response() http.ResponseWriter
// SetResponse replaces the response writer for the remainder of the chain.
// Intended for middleware that wraps the writer (e.g. compression); route
// handlers normally do not need it.
SetResponse(w http.ResponseWriter)
// Identity
Method() string
Path() string
IP() string
// BaseURL returns (protocol + host + base path).
BaseURL() string
// Locale returns the request locale (set by the AcceptLanguage middleware),
// falling back to the i18n default, then "".
Locale() string
// Input
Params(key string) string
Query(key string, def ...string) string
Header(key string, def ...string) string // request header
Body() []byte
FormValue(key string, def ...string) string // body form field (urlencoded or multipart)
Bind(out any) error // JSON body -> out (json.Unmarshal)
//
Translate(locale, key string, args ...any) string // i18n lookup (requires WithI18n)
// Output (SetHeader/Status chain before the body is written)
SetHeader(key, value string) Ctx
Status(code int) Ctx
JSON(v any) error
Send(b []byte) error
SendString(s string) error
Redirect(status int, url string) error
Render(name string, bind any, layouts ...string) error // html template render
// Next runs the next handler in the middleware chain. Middleware call it to
// continue; route handlers are terminal and ignore it.
Next() error
}
// Handler is the typical handler signature: it receives a Ctx and returns an
// error (nil on success). Route registrars (Get/Post/.../All) accept a Handler
// or, for direct stdlib interop, an http.Handler.
type Handler func(ctx Ctx) error
// ctxImpl is the concrete Ctx. Cheap to allocate per request.
type ctxImpl struct {
app *app // back-reference for app-level services (views); nil-safe
w http.ResponseWriter
r *http.Request
body []byte
bodyRead bool
chain []Handler // per-request handler chain (middleware + dispatch)
index int // current position in chain; Next advances it
}
func newCtx(w http.ResponseWriter, r *http.Request) *ctxImpl {
return &ctxImpl{w: w, r: r}
}
// Next runs the next handler in the chain. Middleware use it to continue; the
// terminal dispatch handler does not call it. Returns the next handler's error
// (nil past the end of the chain).
func (c *ctxImpl) Next() error {
c.index++
if c.index >= len(c.chain) {
return nil
}
return c.chain[c.index](c)
}
func (c *ctxImpl) Request() *http.Request { return c.r }
func (c *ctxImpl) Response() http.ResponseWriter { return c.w }
func (c *ctxImpl) SetResponse(w http.ResponseWriter) { c.w = w }
func (c *ctxImpl) Method() string { return c.r.Method }
func (c *ctxImpl) Path() string { return c.r.URL.Path }
func (c *ctxImpl) IP() string { return clientIP(c.r) }
// BaseURL returns (protocol + host + base path). Protocol honors
// X-Forwarded-Proto and TLS; host honors X-Forwarded-Host then r.Host. There is
// no base-path config yet, so the base path is currently empty.
func (c *ctxImpl) BaseURL() string {
scheme := "http"
if c.r.TLS != nil {
scheme = "https"
}
if p := c.r.Header.Get("X-Forwarded-Proto"); p != "" {
scheme = p
}
host := c.r.Host
if h := c.r.Header.Get("X-Forwarded-Host"); h != "" {
host = h
}
return scheme + "://" + host
}
func (c *ctxImpl) Locale() string {
if loc := localeFromRequest(c.r); loc != "" {
return loc
}
if i := c.i18n(); i != nil {
return i.Default()
}
return ""
}
func (c *ctxImpl) Params(key string) string {
return c.r.PathValue(key)
}
func (c *ctxImpl) Query(key string, def ...string) string {
if v := c.r.URL.Query().Get(key); v != "" {
return v
}
if len(def) > 0 {
return def[0]
}
return ""
}
func (c *ctxImpl) Header(key string, def ...string) string {
if v := c.r.Header.Get(key); v != "" {
return v
}
if len(def) > 0 {
return def[0]
}
return ""
}
func (c *ctxImpl) Body() []byte {
c.ensureBody()
return c.body
}
// FormValue returns the first value of a body form field. It supports both
// application/x-www-form-urlencoded and multipart/form-data (delegated to
// net/http's FormValue, body taking precedence over query). An empty result
// falls back to def, then "".
func (c *ctxImpl) FormValue(key string, def ...string) string {
c.ensureBody()
if v := c.r.FormValue(key); v != "" {
return v
}
if len(def) > 0 {
return def[0]
}
return ""
}
// Bind parses the JSON request body into out via json.Unmarshal. It reads the
// cached body (see ensureBody), so it composes with Body/FormValue regardless
// of call order. An empty body is a no-op (returns nil).
func (c *ctxImpl) Bind(out any) error {
c.ensureBody()
if len(c.body) == 0 {
return nil
}
return json.Unmarshal(c.body, out)
}
// ensureBody reads r.Body once (caching it) and restores r.Body to a reader
// over the cache. This makes Body() and FormValue() independent of call order:
// without it the first consumer would drain the request body stream and leave
// nothing for the other.
func (c *ctxImpl) ensureBody() {
if !c.bodyRead {
c.body, _ = io.ReadAll(c.r.Body)
c.bodyRead = true
}
c.r.Body = io.NopCloser(bytes.NewReader(c.body))
}
func (c *ctxImpl) Status(code int) Ctx {
c.w.WriteHeader(code)
return c
}
func (c *ctxImpl) SetHeader(key, value string) Ctx {
c.w.Header().Set(key, value)
return c
}
// JSON encodes v as JSON and writes it. It sets Content-Type to
// "application/json; charset=utf-8" only when none has been set yet, so a
// handler may override it beforehand (e.g. a custom charset or media type like
// "application/problem+json") via SetHeader.
func (c *ctxImpl) JSON(v any) error {
if c.w.Header().Get("Content-Type") == "" {
c.w.Header().Set("Content-Type", "application/json; charset=utf-8")
}
return json.NewEncoder(c.w).Encode(v)
}
func (c *ctxImpl) Send(b []byte) error {
_, err := c.w.Write(b)
return err
}
func (c *ctxImpl) SendString(s string) error {
return c.Send([]byte(s))
}
func (c *ctxImpl) Redirect(status int, url string) error {
c.w.Header().Set("Location", url)
c.w.WriteHeader(status)
return nil
}
// Render renders the named template (with optional layouts) and writes it. It
// sets Content-Type to "text/html; charset=utf-8" only when none has been set
// yet, so a handler may override it beforehand (e.g. for XHTML or a custom
// charset) via SetHeader. Requires a Views engine configured via WithViews.
func (c *ctxImpl) Render(name string, bind any, layouts ...string) error {
v := c.views()
if v == nil {
return errors.New("httpsrv: no views configured (use WithViews)")
}
if c.w.Header().Get("Content-Type") == "" {
c.w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
return v.Render(c.w, name, bind, layouts...)
}
func (c *ctxImpl) views() Views {
if c.app != nil {
if p := c.app.views.Load(); p != nil {
return *p
}
}
return nil
}
// Translate looks up key in locale via the App's i18n store; without one
// (WithI18n not used) it returns the key unchanged.
func (c *ctxImpl) Translate(locale, key string, args ...any) string {
if i := c.i18n(); i != nil {
return i.Translate(locale, key, args...)
}
return key
}
func (c *ctxImpl) i18n() *I18n {
if c.app != nil {
if v := c.app.i18n.Load(); v != nil {
return v
}
}
return nil
}
// toHandler normalizes a registrar argument to a Handler. It accepts a Handler
// (or func(Ctx) error), or an http.Handler wrapped via basicToHandler; anything
// else panics at registration (fail-fast). For a plain stdlib handler function,
// wrap it with http.HandlerFunc explicitly.
func toHandler(method, path string, h any) Handler {
switch fn := h.(type) {
case Handler:
return fn
case func(Ctx) error:
return fn
case http.Handler:
return basicToHandler(fn)
default:
panic("httpsrv: unsupported handler type for " + method + " " + path)
}
}
// basicToHandler adapts a stdlib http.Handler to a Handler: it dispatches to the
// underlying ServeHTTP and returns nil (http.Handlers have no error result).
func basicToHandler(h http.Handler) Handler {
return func(c Ctx) error {
h.ServeHTTP(c.Response(), c.Request())
return nil
}
}
// defaultCtxError is the fallback when a Handler returns an error: respond 500
// with a generic body. The error is logged via slog rather than written to the
// client, since it may disclose internal details (template names/paths, type or
// stack context). Replace this behavior with a custom ErrorHandler
// (WithErrorHandler) if a different response is wanted. If the Handler already
// wrote the response, the superfluous header/body is logged by net/http.
func defaultCtxError(w http.ResponseWriter, err error) {
slog.Error("httpsrv: handler error", "err", err)
h := w.Header()
h.Set("Content-Type", "text/plain; charset=utf-8")
h.Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Internal Server Error")
}
// clientIP extracts the client address, honoring X-Forwarded-For (first hop)
// then X-Real-Ip, falling back to the RemoteAddr host.
func clientIP(r *http.Request) string {
if v := r.Header.Get("X-Forwarded-For"); v != "" {
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
return strings.TrimSpace(v)
}
if v := r.Header.Get("X-Real-Ip"); v != "" {
return strings.TrimSpace(v)
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}