Skip to content

Commit 501992a

Browse files
Add status code recorder to middleware
1 parent c3d72b1 commit 501992a

File tree

10 files changed

+171
-77
lines changed

10 files changed

+171
-77
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ clean:
3232
.PHONY: ci
3333
ci: clean lint test
3434

35-
.DEFAULT_GOAL := build
35+
.DEFAULT_GOAL := api

cmd/internal/constants.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package constants
2+
3+
// Context types
4+
type RepositoryKeyType string
5+
6+
const RepositoryKey RepositoryKeyType = "repo_key"
7+
8+
type DBConnPoolKey string
9+
10+
const DBConnPool DBConnPoolKey = "db_pool_key"
11+
12+
type StatusCodeKeyType string
13+
14+
const StatusCodeKey StatusCodeKeyType = "status_code_key"

cmd/internal/db/middleware.go

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,54 @@ package db
22

33
import (
44
"context"
5+
"fmt"
56
"log"
67
"net/http"
78

89
"github.com/jackc/pgx/v5"
910
"github.com/jackc/pgx/v5/pgxpool"
11+
constants "github.com/simondanielsson/recite/cmd/internal"
1012
"github.com/simondanielsson/recite/cmd/internal/queries"
1113
)
1214

13-
type RepositoryKeyType string
14-
15-
const RepositoryKey RepositoryKeyType = "repo_key"
16-
17-
type statusRecorder struct {
18-
http.ResponseWriter
19-
status int
20-
}
21-
2215
func AddDatabaseMiddleware(handler http.Handler, pool *pgxpool.Pool, logger *log.Logger) http.Handler {
2316
return http.HandlerFunc(
2417
func(w http.ResponseWriter, r *http.Request) {
2518
ctx := r.Context()
26-
tx, err := pool.BeginTx(ctx, pgx.TxOptions{
27-
IsoLevel: pgx.Serializable,
28-
AccessMode: pgx.ReadWrite,
29-
})
19+
repository, tx, err := NewRepositoryWithTx(ctx, pool)
3020
if err != nil {
31-
http.Error(w, "failed to start transaction", http.StatusInternalServerError)
21+
http.Error(w, err.Error(), http.StatusInternalServerError)
3222
}
33-
repository := queries.New(pool).WithTx(tx)
3423

35-
ctx = context.WithValue(ctx, RepositoryKey, repository)
24+
// Attach transaction-scoped repository, and pool for transactions to be launched outside request context
25+
ctx = context.WithValue(ctx, constants.RepositoryKey, repository)
26+
ctx = context.WithValue(ctx, constants.DBConnPool, pool)
3627

37-
rec := statusRecorder{ResponseWriter: w, status: http.StatusOK}
38-
handler.ServeHTTP(rec, r.WithContext(ctx))
28+
handler.ServeHTTP(w, r.WithContext(ctx))
3929

40-
if rec.status >= http.StatusBadRequest {
30+
status, ok := ctx.Value(constants.StatusCodeKey).(int)
31+
if !ok {
32+
_ = tx.Rollback(ctx)
33+
return
34+
}
35+
36+
if status >= http.StatusBadRequest {
4137
_ = tx.Rollback(ctx)
4238
} else if err := tx.Commit(ctx); err != nil {
4339
// Response has already been sent - just log
44-
logger.Printf("commit tx failed: %v", err)
40+
logger.Printf("tx commit failed: %v", err)
4541
}
4642
},
4743
)
4844
}
45+
46+
func NewRepositoryWithTx(ctx context.Context, pool *pgxpool.Pool) (*queries.Queries, pgx.Tx, error) {
47+
tx, err := pool.BeginTx(ctx, pgx.TxOptions{
48+
IsoLevel: pgx.Serializable,
49+
AccessMode: pgx.ReadWrite,
50+
})
51+
if err != nil {
52+
return nil, nil, fmt.Errorf("failed to start transaction: %w", err)
53+
}
54+
return queries.New(pool).WithTx(tx), tx, nil
55+
}

cmd/internal/logging/logging.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,12 @@ package logging
33
import (
44
"io"
55
"log"
6-
"net/http"
7-
"time"
86
)
97

8+
const DateFormat string = "Mon Jan 2 15:04:05 MST 2006"
9+
1010
func NewLogger(w io.Writer) *log.Logger {
1111
logger := log.Logger{}
1212
logger.SetOutput(w)
1313
return &logger
1414
}
15-
16-
func AddLoggingMiddleware(handler http.Handler, logger *log.Logger) http.Handler {
17-
return http.HandlerFunc(
18-
func(w http.ResponseWriter, r *http.Request) {
19-
handler.ServeHTTP(w, r)
20-
logger.Printf("%s | %s %s\n", time.Now().Format("Mon Jan 2 15:04:05 MST 2006"), r.Method, r.URL)
21-
},
22-
)
23-
}

cmd/internal/logging/middleware.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package logging
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"time"
7+
8+
constants "github.com/simondanielsson/recite/cmd/internal"
9+
)
10+
11+
func AddLoggingMiddleware(handler http.Handler, logger *log.Logger) http.Handler {
12+
return http.HandlerFunc(
13+
func(w http.ResponseWriter, r *http.Request) {
14+
handler.ServeHTTP(w, r)
15+
status, ok := r.Context().Value(constants.StatusCodeKey).(int)
16+
if !ok {
17+
// TODO: write proper error, or assume it failed
18+
panic("could not get status")
19+
}
20+
21+
logger.Printf("%s | %s %s - %d\n", time.Now().Format(DateFormat), r.Method, r.URL, status)
22+
},
23+
)
24+
}

cmd/internal/marshal/marshal.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package marshal
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"net/http"
8+
9+
constants "github.com/simondanielsson/recite/cmd/internal"
710
)
811

9-
func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error {
12+
func Encode[T any](ctx context.Context, w http.ResponseWriter, r *http.Request, status int, v T) error {
1013
w.Header().Set("Content-Type", "application/json")
1114
w.WriteHeader(status)
15+
16+
// A bit hacky way to write override the existing context
17+
ctx = context.WithValue(ctx, constants.StatusCodeKey, status)
18+
*r = *(r.WithContext(ctx))
19+
1220
if err := json.NewEncoder(w).Encode(v); err != nil {
1321
return fmt.Errorf("encode json: %w", err)
1422
}

cmd/internal/routes/routes.go

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package routes
22

33
import (
4+
"context"
45
"fmt"
56
"log"
67
"net/http"
78
"strconv"
89

9-
"github.com/simondanielsson/recite/cmd/internal/db"
10+
"github.com/jackc/pgx/v5/pgxpool"
11+
constants "github.com/simondanielsson/recite/cmd/internal"
1012
"github.com/simondanielsson/recite/cmd/internal/marshal"
1113
"github.com/simondanielsson/recite/cmd/internal/queries"
1214
"github.com/simondanielsson/recite/cmd/internal/services"
@@ -28,6 +30,7 @@ func rootGetHandler(logger *log.Logger) http.Handler {
2830
func(w http.ResponseWriter, r *http.Request) {
2931
w.WriteHeader(http.StatusOK)
3032
if _, err := w.Write([]byte("ok\n")); err != nil {
33+
logger.Println(err)
3134
w.WriteHeader(http.StatusInternalServerError)
3235
}
3336
},
@@ -49,7 +52,7 @@ func recitalPostHandler(logger *log.Logger) http.Handler {
4952
res := messageResponse{
5053
Message: fmt.Sprintf("bad request, should contain url. Got %s", req.Url),
5154
}
52-
if err := marshal.Encode(w, r, http.StatusBadRequest, res); err != nil {
55+
if err := marshal.Encode(r.Context(), w, r, http.StatusBadRequest, res); err != nil {
5356
writeErrHeader(w, err, logger)
5457
}
5558
return
@@ -62,71 +65,69 @@ func recitalPostHandler(logger *log.Logger) http.Handler {
6265
// 4. Build a simple voice streamer in a basic htmx web page
6366

6467
ctx := r.Context()
65-
repository, ok := ctx.Value(db.RepositoryKey).(*queries.Queries)
68+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
6669
if !ok {
67-
res := messageResponse{Message: "Something went wrong."}
68-
logger.Print(err)
69-
if err := marshal.Encode(w, r, int(http.StatusInternalServerError), res); err != nil {
70-
writeErrHeader(w, err, logger)
71-
return
72-
}
70+
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
71+
return
72+
}
73+
pool, ok := ctx.Value(constants.DBConnPool).(*pgxpool.Pool)
74+
if !ok {
75+
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading db connection pool"), logger)
76+
return
7377
}
7478

75-
id, err := services.CreateRecital(ctx, req.Url, repository, logger)
79+
id, err := services.CreateRecital(ctx, req.Url, repository, pool, logger)
7680
if err != nil {
77-
res := messageResponse{Message: "Something went wrong."}
78-
logger.Print(err)
79-
if err := marshal.Encode(w, r, int(http.StatusInternalServerError), res); err != nil {
80-
writeErrHeader(w, err, logger)
81-
return
82-
}
81+
logGenericInternalServiceError(ctx, w, r, err, logger)
82+
return
8383
}
8484

8585
res := response{
8686
Id: id,
8787
}
88-
if err := marshal.Encode(w, r, http.StatusCreated, res); err != nil {
88+
if err := marshal.Encode(ctx, w, r, http.StatusCreated, res); err != nil {
8989
writeErrHeader(w, err, logger)
9090
return
9191
}
9292
},
9393
)
9494
}
9595

96-
// TODO: move
97-
type GenerationStatus string
98-
99-
const (
100-
InProgress GenerationStatus = "in progress"
101-
Completed GenerationStatus = "completed"
102-
Failed GenerationStatus = "failed"
103-
)
104-
10596
func recitalGetHandler(logger *log.Logger) http.Handler {
10697
type response struct {
107-
Status GenerationStatus
98+
queries.Recital
10899
}
109100
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110101
id, err := strconv.Atoi(r.PathValue("id"))
111102
if err != nil {
112103
res := messageResponse{Message: "invalid id"}
113-
if err := marshal.Encode(w, r, http.StatusBadRequest, res); err != nil {
104+
if err := marshal.Encode(r.Context(), w, r, http.StatusBadRequest, res); err != nil {
114105
writeErrHeader(w, err, logger)
115-
return
116106
}
107+
return
117108
}
118109

119-
_ = id
120-
// TODO: unmock
121-
res := response{Status: Completed}
122-
if err := marshal.Encode(w, r, http.StatusOK, res); err != nil {
123-
res := messageResponse{"failed to encode response"}
124-
if err := marshal.Encode(w, r, http.StatusInternalServerError, res); err != nil {
125-
writeErrHeader(w, err, logger)
110+
ctx := r.Context()
111+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
112+
if !ok {
113+
logGenericInternalServiceError(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
114+
}
115+
116+
recital, err := services.GetRecital(ctx, int32(id), repository, logger)
117+
if err != nil {
118+
res := messageResponse{Message: fmt.Sprintf("Could not find recital with id %d", id)}
119+
if err := marshal.Encode(ctx, w, r, http.StatusNotFound, res); err != nil {
120+
logFailedEncodingResponse(ctx, w, r, err, logger)
126121
}
122+
return
127123
}
128124

129-
return
125+
res := response{
126+
Recital: recital,
127+
}
128+
if err := marshal.Encode(ctx, w, r, http.StatusOK, res); err != nil {
129+
logFailedEncodingResponse(ctx, w, r, err, logger)
130+
}
130131
})
131132
}
132133

@@ -136,3 +137,20 @@ func writeErrHeader(w http.ResponseWriter, err error, logger *log.Logger) {
136137
logger.Print("failed writing error")
137138
}
138139
}
140+
141+
func logGenericInternalServiceError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger *log.Logger) {
142+
res := messageResponse{Message: "Something went wrong."}
143+
logger.Print(err)
144+
if err := marshal.Encode(ctx, w, r, int(http.StatusInternalServerError), res); err != nil {
145+
writeErrHeader(w, err, logger)
146+
return
147+
}
148+
}
149+
150+
func logFailedEncodingResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, logger *log.Logger) {
151+
logger.Println(err)
152+
res := messageResponse{"failed to encode response"}
153+
if err := marshal.Encode(ctx, w, r, http.StatusInternalServerError, res); err != nil {
154+
writeErrHeader(w, err, logger)
155+
}
156+
}

cmd/internal/services/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,11 @@ import "path"
55
const maxArticleContentCharLength int = 3500
66

77
var baseOutputPath string = path.Join("data", "output", "generations", "audio")
8+
9+
type Status string
10+
11+
const (
12+
InProgress Status = "in progress"
13+
Completed Status = "completed"
14+
Failed Status = "failed"
15+
)

0 commit comments

Comments
 (0)