-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandle.go
153 lines (137 loc) · 5.41 KB
/
handle.go
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
package genhttp
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// ResponseCreator is a factory type that is capable of generating new
// Responder instances that are ready to be used.
//
// We use a type parameter here instead of using the interface directly because
// we want the response types to be able to be structs with encoding tags or
// whatever, and to have their fields be directly accessible without a type
// assertion, which the Responder interface isn't able to provide.
type ResponseCreator[Response Responder] interface {
NewResponse(context.Context, *http.Request) Response
}
// Responder is an interface describing a response type.
type Responder interface {
// HasErrors should return true if the response is considered an error
// response and processing should not proceed.
HasErrors() bool
// Send writes the response to the http.ResponseWriter.
Send(context.Context, http.ResponseWriter)
// HandlePanic updates the Response in the face of a panic while
// processing the request.
HandlePanic(ctx context.Context, recoverArg any)
}
// Redirecter is an interface describing a Responder that sometimes responds by
// redirecting the client.
type Redirecter interface {
// RedirectTo returns the URL to redirect to and the HTTP status code
// to use when redirecting. If the status code returned is between 300
// and 399, inclusive, genhttp will call http.Redirect with the
// returned URL and status code instead of calling Send.
RedirectTo() (url string, status int)
}
// CookieWriter is an interface describing a Responder that sometimes responds
// by writing cookies to the client.
type CookieWriter interface {
// WriteCookies returns the cookies to write. An attempt is always made
// to write cookies.
WriteCookies() []*http.Cookie
}
// Handler is an endpoint that is going to parse, validate, and execute an HTTP
// request. Its Request type parameter should be a type that can describe the
// request, usually a struct with JSON tags or something similar. The Response
// type parameter should be an implementation of the Responder interface,
// usually a pointer that can be modified in the ParseRequest, ValidateRequest,
// and ExecuteRequest methods.
type Handler[Request any, Response Responder] interface {
// ParseRequest turns an `http.Request` into the Request type passed
// in, usually by parsing some encoding. The returned context.Context
// will be used as the new request context; if in doubt, return the
// context.Context passed as an argument.
ParseRequest(context.Context, *http.Request, Response) (Request, context.Context)
// ValidateRequest checks that the passed Request is valid, for
// whatever definition of valid suits the endpoint. The returned
// context.Context will be used as the new request context; if in
// doubt, return the context.Context passed as an argument.
ValidateRequest(context.Context, Request, Response) context.Context
// ExecuteRequest performs the action described by the Request. It can
// assume that the Request is valid. The returned context.Context will
// be used as the new request context; if in doubt, return the
// context.Context passed as an argument.
ExecuteRequest(context.Context, Request, Response) context.Context
}
// Handle provides an http.Handler that will call the passed Handler. `rf` is
// used to create a new instance of the Response type. Then the `ParseRequest`
// method of `h` will be called, followed by `ValidateRequest` and
// `ExecuteRequest`. At the end, the Response has its `Send` method called.
//
// If at any point in this process the Response's `HasErrors` method returns
// `true`, the Response's `Send` method is called and the function returns.
func Handle[Request any, Response Responder](respCreator ResponseCreator[Response], handler Handler[Request, Response]) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.GetTracerProvider().Tracer("impractical.co/genhttp")
var span trace.Span
ctx, span = tracer.Start(ctx, "handleRequest")
defer span.End()
resp := respCreator.NewResponse(ctx, r)
defer func() {
if cw, ok := any(resp).(CookieWriter); ok {
for _, cookie := range cw.WriteCookies() {
http.SetCookie(w, cookie)
}
}
if red, ok := any(resp).(Redirecter); ok {
url, code := red.RedirectTo()
if code >= 300 && code < 400 {
http.Redirect(w, r, url, code)
return
}
}
resp.Send(ctx, w)
}()
defer func() {
msg := recover()
if msg == nil {
return
}
resp.HandlePanic(ctx, msg)
}()
if resp.HasErrors() {
return
}
// if we know we're going to redirect when we instantiate the
// response, do the redirect
if red, ok := any(resp).(Redirecter); ok {
if _, code := red.RedirectTo(); code >= 300 && code < 400 {
// don't need to redirect here, just let the
// defer handle it
return
}
}
var req Request
var parseSpan trace.Span
ctx, parseSpan = tracer.Start(ctx, "parseRequest")
req, ctx = handler.ParseRequest(ctx, r, resp)
parseSpan.End()
if resp.HasErrors() {
return
}
var validateSpan trace.Span
ctx, validateSpan = tracer.Start(ctx, "validateRequest")
ctx = handler.ValidateRequest(ctx, req, resp)
validateSpan.End()
if resp.HasErrors() {
return
}
var execSpan trace.Span
ctx, execSpan = tracer.Start(ctx, "executeRequest")
ctx = handler.ExecuteRequest(ctx, req, resp)
execSpan.End()
})
}