Skip to content

Commit e78daf0

Browse files
committed
Add CodeErr.Code method. Modernize a bit of the code. Update doc comments.
1 parent fdcd237 commit e78daf0

File tree

11 files changed

+129
-56
lines changed

11 files changed

+129
-56
lines changed

err.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import (
77
"net/http"
88
)
99

10-
// Errf is a convenience wrapper for http.Error.
10+
// Errf is a convenience wrapper for [http.Error].
1111
// It calls http.Error(w, fmt.Sprintf(format, args...), code).
12-
// It also logs that message with log.Print.
13-
// If code is 0, it defaults to http.StatusInternalServerError.
14-
// If format is "", Errf uses http.StatusText instead.
12+
// It also logs that message with [log.Print].
13+
// If code is 0, it defaults to [http.StatusInternalServerError].
14+
// If format is "", Errf uses [http.StatusText] instead.
1515
func Errf(w http.ResponseWriter, code int, format string, args ...interface{}) {
1616
if code == 0 {
1717
code = http.StatusInternalServerError
@@ -28,14 +28,14 @@ func Errf(w http.ResponseWriter, code int, format string, args ...interface{}) {
2828
http.Error(w, msg, code)
2929
}
3030

31-
// Err wraps an error-returning function as an http.Handler.
32-
// If the returned error is a Responder (such as a CodeErr),
31+
// Err wraps an error-returning function as an [http.Handler].
32+
// If the returned error is a [Responder] (such as a [CodeErr]),
3333
// its Respond method is used to respond to the request.
3434
// Otherwise,
3535
// if a status code has not already been set,
36-
// an error return will set it to http.StatusInternalServerError,
37-
// and the absence of an error will set it to http.StatusOK,
38-
// or http.StatusNoContent if nothing has been written to the ResponseWriter.
36+
// an error return will set it to [http.StatusInternalServerError],
37+
// and the absence of an error will set it to [http.StatusOK],
38+
// or [http.StatusNoContent] if nothing has been written to the ResponseWriter.
3939
func Err(f func(http.ResponseWriter, *http.Request) error) http.Handler {
4040
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
4141
ww := ResponseWrapper{W: w}
@@ -53,7 +53,7 @@ func Err(f func(http.ResponseWriter, *http.Request) error) http.Handler {
5353
})
5454
}
5555

56-
// CodeErr is an error that can be returned from the function wrapped by Err
56+
// CodeErr is an error that can be returned from the function wrapped by [Err]
5757
// to control the HTTP status code returned from the pending request.
5858
type CodeErr struct {
5959
// C is an HTTP status code.
@@ -75,12 +75,12 @@ func (c CodeErr) Error() string {
7575
return s
7676
}
7777

78-
// Unwrap implements the interface for errors.Unwrap.
78+
// Unwrap implements the interface for [errors.Unwrap].
7979
func (c CodeErr) Unwrap() error {
8080
return c.Err
8181
}
8282

83-
// As implements the interface for errors.As.
83+
// As implements the interface for [errors.As].
8484
func (c CodeErr) As(target interface{}) bool {
8585
if ptr, ok := target.(*CodeErr); ok {
8686
*ptr = c
@@ -89,14 +89,19 @@ func (c CodeErr) As(target interface{}) bool {
8989
return false
9090
}
9191

92-
// Respond implements Responder.
92+
// Respond implements [Responder].
9393
func (c CodeErr) Respond(w http.ResponseWriter) {
9494
http.Error(w, c.Error(), c.C)
9595
}
9696

97+
// Code returns the HTTP status code.
98+
func (c CodeErr) Code() int {
99+
return c.C
100+
}
101+
97102
// Responder is an interface for objects that know how to respond to an HTTP request.
98103
// It is useful in the case of errors that want to set custom error strings and/or status codes
99-
// (e.g. via http.Error).
104+
// (e.g. via [http.Error]).
100105
type Responder interface {
101106
Respond(http.ResponseWriter)
102107
}

err_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package mid
22

33
import (
44
"errors"
5-
"io/ioutil"
5+
"io"
66
"net/http"
77
"net/http/httptest"
88
"testing"
@@ -74,7 +74,7 @@ func TestErr(t *testing.T) {
7474
t.Errorf("got code %d, want %d", resp.StatusCode, c.wantCode)
7575
}
7676
if c.wantBody != "" {
77-
body, err := ioutil.ReadAll(resp.Body)
77+
body, err := io.ReadAll(resp.Body)
7878
if err != nil {
7979
t.Fatal(err)
8080
}
@@ -85,3 +85,10 @@ func TestErr(t *testing.T) {
8585
})
8686
}
8787
}
88+
89+
func TestCode(t *testing.T) {
90+
e := CodeErr{C: http.StatusTeapot}
91+
if got := e.Code(); got != http.StatusTeapot {
92+
t.Errorf("got code %d, want %d", got, http.StatusTeapot)
93+
}
94+
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module github.com/bobg/mid
22

3-
go 1.14
3+
go 1.21
44

55
require (
6+
github.com/bobg/errors v1.1.0
67
github.com/google/go-cmp v0.5.1
7-
github.com/pkg/errors v0.9.1
88
)
99

1010
retract v1.5.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
github.com/bobg/errors v1.1.0 h1:gsVanPzJMpZQpwY+27/GQYElZez5CuMYwiIpk2A3RGw=
2+
github.com/bobg/errors v1.1.0/go.mod h1:Q4775qBZpnte7EGFJqmvnlB1U4pkI1XmU3qxqdp7Zcc=
13
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
24
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
4-
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
55
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
66
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

json.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"reflect"
1010
"strings"
1111

12-
"github.com/pkg/errors"
12+
"github.com/bobg/errors"
1313
)
1414

1515
var (
@@ -22,7 +22,7 @@ type (
2222
respKey struct{}
2323
)
2424

25-
// JSON produces an http.Handler by JSON encoding and decoding of a given function's input and output.
25+
// JSON produces an [http.Handler] by JSON encoding and decoding of a given function's input and output.
2626
//
2727
// The signature of the function is:
2828
//
@@ -40,17 +40,17 @@ type (
4040
// When the function is called:
4141
//
4242
// - If a context argument is present,
43-
// it is supplied from the Context() method of the pending *http.Request.
43+
// it is supplied from the Context() method of the pending [*http.Request].
4444
// That context is further adorned with the pending *http.Request
45-
// and the pending http.ResponseWriter,
46-
// which can be retrieved with the Request and ResponseWriter functions.
45+
// and the pending [http.ResponseWriter],
46+
// which can be retrieved with the [Request] and [ResponseWriter] functions.
4747
//
4848
// - If an inType argument is present,
4949
// the request is checked to ensure that the method is POST
5050
// and the Content-Type is application/json;
5151
// then the request body is unmarshaled into the inType argument.
5252
// Note that the JSON decoder uses the UseNumber setting;
53-
// see https://golang.org/pkg/encoding/json/#Decoder.UseNumber.
53+
// see [json.Decoder.UseNumber].
5454
//
5555
// - If an outType result is present,
5656
// it is JSON marshaled and written to the pending ResponseWriter
@@ -59,7 +59,7 @@ type (
5959
// the default HTTP status is 204 (no content).
6060
//
6161
// - If an error result is present,
62-
// it is handled as in Err.
62+
// it is handled as in [Err].
6363
//
6464
// Some of the code in this function is (liberally) adapted from github.com/chain/chain.
6565
func JSON(f interface{}) http.Handler {

limit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ type Limiter interface {
1313
Wait(context.Context) error
1414
}
1515

16-
// LimitedTransport is an http.RoundTripper that limits the rate of requests it makes using a [Limiter].
16+
// LimitedTransport is an [http.RoundTripper] that limits the rate of requests it makes using a [Limiter].
1717
// After waiting for the limiter in L, it delegates to the http.RoundTripper in T.
1818
// If T is nil, it uses [http.DefaultTransport].
1919
type LimitedTransport struct {
2020
L Limiter
2121
T http.RoundTripper
2222
}
2323

24-
// RoundTrip implements the http.RoundTripper interface.
24+
// RoundTrip implements the [http.RoundTripper] interface.
2525
func (lt LimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
2626
if err := lt.L.Wait(req.Context()); err != nil {
2727
return nil, err

limit_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package mid
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"testing"
9+
)
10+
11+
func TestLimiter(t *testing.T) {
12+
cases := []struct {
13+
waitErr, rtErr error
14+
}{{
15+
waitErr: nil,
16+
rtErr: nil,
17+
}, {
18+
waitErr: errors.New("wait error"),
19+
}, {
20+
rtErr: errors.New("round trip error"),
21+
}}
22+
23+
for i, tc := range cases {
24+
t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) {
25+
var (
26+
lim = mockLimiter{waitErr: tc.waitErr}
27+
transp = mockTransport{rtErr: tc.rtErr}
28+
lt = LimitedTransport{L: lim, T: transp}
29+
)
30+
31+
_, err := lt.RoundTrip(&http.Request{})
32+
33+
switch {
34+
case tc.waitErr != nil:
35+
if !errors.Is(err, tc.waitErr) {
36+
t.Errorf("got %v, want %v", err, tc.waitErr)
37+
}
38+
39+
case tc.rtErr != nil:
40+
if !errors.Is(err, tc.rtErr) {
41+
t.Errorf("got %v, want %v", err, tc.rtErr)
42+
}
43+
44+
default:
45+
if err != nil {
46+
t.Errorf("got %v, want nil", err)
47+
}
48+
}
49+
})
50+
}
51+
}
52+
53+
type mockLimiter struct {
54+
waitErr error
55+
}
56+
57+
func (ml mockLimiter) Wait(context.Context) error {
58+
return ml.waitErr
59+
}
60+
61+
type mockTransport struct {
62+
rtErr error
63+
}
64+
65+
func (mt mockTransport) RoundTrip(*http.Request) (*http.Response, error) {
66+
return nil, mt.rtErr
67+
}

log.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import (
55
"net/http"
66
)
77

8-
// Log adds logging on entry to and exit from an http.Handler.
8+
// Log adds logging on entry to and exit from an [http.Handler] using [log.Printf].
99
//
1010
// If the request is decorated with a trace ID
11-
// (see Trace),
11+
// (see [Trace]),
1212
// it is included in the generated log lines.
1313
func Log(next http.Handler) http.Handler {
1414
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

mid.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
// Package mid contains assorted middleware for use in HTTP services.
22
package mid
33

4-
import (
5-
"net/http"
6-
)
4+
import "net/http"
75

8-
// ResponseWrapper implements http.ResponseWriter,
6+
// ResponseWrapper implements [http.ResponseWriter],
97
// delegating calls to a wrapped http.ResponseWriter object.
108
// It also records the status code and the number of response bytes that have been written.
119
type ResponseWrapper struct {

session.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import (
1010
"net/http"
1111
"time"
1212

13-
"github.com/pkg/errors"
13+
"github.com/bobg/errors"
1414
)
1515

16-
// Session is the type of a session stored in a SessionStore.
16+
// Session is the type of a session stored in a [SessionStore].
1717
type Session interface {
1818
// CSRFKey is a persistent random bytestring that can be used for CSRF protection.
1919
CSRFKey() [sha256.Size]byte
@@ -48,7 +48,7 @@ func CSRFToken(s Session) (string, error) {
4848
return base64.RawURLEncoding.EncodeToString(buf[:]), nil
4949
}
5050

51-
// ErrCSRF is the error produced when an invalid CSRF token is presented to CSRFCheck.
51+
// ErrCSRF is the error produced when an invalid CSRF token is presented to [CSRFCheck].
5252
var ErrCSRF = errors.New("CSRF check failed")
5353

5454
// CSRFCheck checks a CSRF token against a session for validity.
@@ -81,13 +81,13 @@ func csrfSum(s Session, inp []byte) ([]byte, error) {
8181
return h.Sum(nil), nil
8282
}
8383

84-
// ErrNoSession is the error produced by SessionStore.Get when no matching session is found.
84+
// ErrNoSession is the error produced by [SessionStore.Get] when no matching session is found.
8585
var ErrNoSession = errors.New("no session")
8686

8787
// SessionStore is persistent storage for session objects.
8888
type SessionStore interface {
8989
// Get gets the session with the given key.
90-
// If no such session is found, it returns ErrNoSession.
90+
// If no such session is found, it returns [ErrNoSession].
9191
Get(context.Context, string) (Session, error)
9292

9393
// Cancel cancels the session with the given unique key.
@@ -106,15 +106,15 @@ func GetSession(ctx context.Context, store SessionStore, cookieName string, req
106106
return store.Get(ctx, cookie.Value)
107107
}
108108

109-
// IsNoSession tests whether the given error is either ErrNoSession or http.ErrNoCookie.
109+
// IsNoSession tests whether the given error is either [ErrNoSession] or [http.ErrNoCookie].
110110
func IsNoSession(err error) bool {
111111
return errors.Is(err, http.ErrNoCookie) || errors.Is(err, ErrNoSession)
112112
}
113113

114-
// SessionHandler is an http.Handler middleware wrapper.
114+
// SessionHandler is an [http.Handler] middleware wrapper.
115115
// It checks the incoming request for a session in the given store.
116116
// If one is found, the request's context is decorated with the session.
117-
// It can be retrieved by the next handler with ContextSession.
117+
// It can be retrieved by the next handler with [ContextSession].
118118
// If an active, unexpired session is not found, a 403 Forbidden error is returned.
119119
func SessionHandler(store SessionStore, cookieName string, next http.Handler) http.Handler {
120120
return Err(func(w http.ResponseWriter, req *http.Request) error {
@@ -129,7 +129,7 @@ func SessionHandler(store SessionStore, cookieName string, next http.Handler) ht
129129
if !s.Active() || s.Exp().Before(time.Now()) {
130130
return CodeErr{C: http.StatusForbidden, Err: fmt.Errorf("session inactive or expired")}
131131
}
132-
ctx = context.WithValue(ctx, sessKey, s)
132+
ctx = context.WithValue(ctx, sessKeyType{}, s)
133133
req = req.WithContext(ctx)
134134
next.ServeHTTP(w, req)
135135
return nil
@@ -138,11 +138,9 @@ func SessionHandler(store SessionStore, cookieName string, next http.Handler) ht
138138

139139
type sessKeyType struct{}
140140

141-
var sessKey sessKeyType
142-
143-
// ContextSession returns the Session associated with a context (by SessionHandler), if there is one.
141+
// ContextSession returns the [Session] associated with a context (by [SessionHandler]), if there is one.
144142
// If there isn't, this returns nil.
145143
func ContextSession(ctx context.Context) Session {
146-
s, _ := ctx.Value(sessKey).(Session)
144+
s, _ := ctx.Value(sessKeyType{}).(Session)
147145
return s
148146
}

0 commit comments

Comments
 (0)