Skip to content

Commit 7a620fa

Browse files
committed
refactor: replace custom request/response types with chi router
Eliminate the hand-rolled Request/Response abstraction layer in favor of a standard net/http + chi router. All entry points (server, lambda, verify, sample) now construct stdlib *http.Request objects and route them through a shared chi handler returned by App.Handler(). This removes the custom HandleRequest dispatcher, consolidates routing and middleware (admin auth, panic recovery), and makes tests more realistic by exercising the actual router via httptest.
1 parent 2dc32b5 commit 7a620fa

File tree

10 files changed

+339
-350
lines changed

10 files changed

+339
-350
lines changed

cmd/lambda/main.go

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
7+
"fmt"
68
"log/slog"
9+
"net/http"
10+
"net/http/httptest"
711
"strings"
812
"sync"
913

@@ -17,6 +21,7 @@ import (
1721
var (
1822
initOnce sync.Once
1923
appInst *app.App
24+
router http.Handler
2025
logger *slog.Logger
2126
initErr error
2227
)
@@ -31,10 +36,15 @@ func initApp() {
3136
return
3237
}
3338
appInst, initErr = app.NewApp(context.Background(), cfg, logger)
39+
if initErr != nil {
40+
return
41+
}
42+
router = appInst.Handler()
3443
})
3544
}
3645

37-
// APIGatewayHandler converts API Gateway requests to unified app.Request.
46+
// APIGatewayHandler converts API Gateway requests to stdlib *http.Request
47+
// and routes them through the chi router.
3848
func APIGatewayHandler(ctx context.Context, req awsevents.APIGatewayV2HTTPRequest) (awsevents.APIGatewayV2HTTPResponse, error) {
3949
initApp()
4050
if initErr != nil {
@@ -50,29 +60,35 @@ func APIGatewayHandler(ctx context.Context, req awsevents.APIGatewayV2HTTPReques
5060
logger.Debug("received api gateway request", slog.String("request", string(j)))
5161
}
5262

53-
headers := make(map[string]string)
54-
for key, value := range req.Headers {
55-
headers[strings.ToLower(key)] = value
63+
httpReq, err := http.NewRequestWithContext(
64+
ctx,
65+
req.RequestContext.HTTP.Method,
66+
req.RawPath,
67+
strings.NewReader(req.Body),
68+
)
69+
if err != nil {
70+
return awsevents.APIGatewayV2HTTPResponse{
71+
StatusCode: 500,
72+
Body: "failed to construct http request",
73+
}, nil
5674
}
5775

58-
appReq := app.Request{
59-
Type: app.RequestTypeHTTP,
60-
Method: req.RequestContext.HTTP.Method,
61-
Path: req.RawPath,
62-
Headers: headers,
63-
Body: []byte(req.Body),
76+
for key, value := range req.Headers {
77+
httpReq.Header.Set(key, value)
6478
}
6579

66-
resp := appInst.HandleRequest(ctx, appReq)
80+
rec := httptest.NewRecorder()
81+
router.ServeHTTP(rec, httpReq)
6782

6883
return awsevents.APIGatewayV2HTTPResponse{
69-
StatusCode: resp.StatusCode,
70-
Headers: resp.Headers,
71-
Body: string(resp.Body),
84+
StatusCode: rec.Code,
85+
Headers: flattenHeaders(rec.Header()),
86+
Body: rec.Body.String(),
7287
}, nil
7388
}
7489

75-
// EventBridgeHandler converts EventBridge events to unified app.Request.
90+
// EventBridgeHandler converts EventBridge events to POST /scheduled/{action}
91+
// requests and routes them through the chi router.
7692
func EventBridgeHandler(ctx context.Context, evt awsevents.CloudWatchEvent) error {
7793
initApp()
7894
if initErr != nil {
@@ -90,16 +106,30 @@ func EventBridgeHandler(ctx context.Context, evt awsevents.CloudWatchEvent) erro
90106
return err
91107
}
92108

93-
req := app.Request{
94-
Type: app.RequestTypeScheduled,
95-
ScheduledAction: detail.Action,
96-
ScheduledData: detail.Data,
109+
path := fmt.Sprintf("%s/scheduled/%s", appInst.Config.BasePath, detail.Action)
110+
111+
var body []byte
112+
if detail.Data != nil {
113+
body = detail.Data
97114
}
98115

99-
resp := appInst.HandleRequest(ctx, req)
116+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, path, bytes.NewReader(body))
117+
if err != nil {
118+
return errors.Wrap(err, "failed to construct http request")
119+
}
100120

101-
if resp.StatusCode >= 400 {
102-
return errors.Newf("scheduled event failed: %s", string(resp.Body))
121+
if appInst.Config.AdminToken != "" {
122+
httpReq.Header.Set("Authorization", "Bearer "+appInst.Config.AdminToken)
123+
}
124+
if len(body) > 0 {
125+
httpReq.Header.Set("Content-Type", "application/json")
126+
}
127+
128+
rec := httptest.NewRecorder()
129+
router.ServeHTTP(rec, httpReq)
130+
131+
if rec.Code >= 400 {
132+
return errors.Newf("scheduled event failed: %s", rec.Body.String())
103133
}
104134

105135
return nil
@@ -125,6 +155,17 @@ func UniversalHandler(ctx context.Context, event json.RawMessage) (any, error) {
125155
return nil, errors.New("unknown lambda event type")
126156
}
127157

158+
// flattenHeaders converts multi-value http.Header to single-value map.
159+
func flattenHeaders(h http.Header) map[string]string {
160+
flat := make(map[string]string, len(h))
161+
for key, values := range h {
162+
if len(values) > 0 {
163+
flat[key] = values[0]
164+
}
165+
}
166+
return flat
167+
}
168+
128169
func main() {
129170
lambda.Start(UniversalHandler)
130171
}

cmd/sample/main.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
package main
55

66
import (
7+
"bytes"
78
"context"
89
"encoding/json"
10+
"fmt"
911
"log/slog"
12+
"net/http"
13+
"net/http/httptest"
1014
"os"
1115
"path/filepath"
1216

@@ -38,6 +42,8 @@ func main() {
3842
os.Exit(1)
3943
}
4044

45+
router := a.Handler()
46+
4147
path := filepath.Join("fixtures", "samples.json")
4248
raw, err := os.ReadFile(path)
4349
if err != nil {
@@ -60,24 +66,45 @@ func main() {
6066

6167
switch eventType {
6268
case "okta_sync":
63-
evt := app.ScheduledEvent{
64-
Action: "okta-sync",
69+
reqPath := fmt.Sprintf("%s/scheduled/okta-sync", cfg.BasePath)
70+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, reqPath, nil)
71+
if err != nil {
72+
logger.Error("failed to construct http request",
73+
slog.Int("sample", i),
74+
slog.String("error", err.Error()))
75+
os.Exit(1)
76+
}
77+
if cfg.AdminToken != "" {
78+
httpReq.Header.Set("Authorization", "Bearer "+cfg.AdminToken)
6579
}
66-
if err := a.ProcessScheduledEvent(ctx, evt); err != nil {
80+
rec := httptest.NewRecorder()
81+
router.ServeHTTP(rec, httpReq)
82+
if rec.Code >= 400 {
6783
logger.Error("failed to process okta_sync sample",
6884
slog.Int("sample", i),
69-
slog.String("error", err.Error()))
85+
slog.String("response", rec.Body.String()))
7086
os.Exit(1)
7187
}
7288

7389
case "pr_webhook":
7490
payload, _ := json.Marshal(sample["payload"])
75-
if err := a.ProcessWebhook(ctx, payload, "pull_request"); err != nil {
76-
logger.Error("failed to process pr_webhook sample",
91+
reqPath := fmt.Sprintf("%s/webhooks", cfg.BasePath)
92+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, reqPath, bytes.NewReader(payload))
93+
if err != nil {
94+
logger.Error("failed to construct http request",
7795
slog.Int("sample", i),
7896
slog.String("error", err.Error()))
7997
os.Exit(1)
8098
}
99+
httpReq.Header.Set("X-GitHub-Event", "pull_request")
100+
rec := httptest.NewRecorder()
101+
router.ServeHTTP(rec, httpReq)
102+
if rec.Code >= 400 {
103+
logger.Error("failed to process pr_webhook sample",
104+
slog.Int("sample", i),
105+
slog.String("response", rec.Body.String()))
106+
os.Exit(1)
107+
}
81108

82109
default:
83110
logger.Info("skipping unknown event type", slog.String("event_type", eventType))

cmd/server/main.go

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@ package main
22

33
import (
44
"context"
5-
"io"
65
"log/slog"
76
"net/http"
87
"os"
98
"os/signal"
10-
"strings"
119
"syscall"
1210
"time"
1311

1412
"github.com/cruxstack/github-ops-app/internal/app"
1513
"github.com/cruxstack/github-ops-app/internal/config"
1614
)
1715

18-
var (
19-
appInst *app.App
20-
logger *slog.Logger
21-
)
22-
2316
func main() {
24-
logger = config.NewLogger()
17+
logger := config.NewLogger()
2518
ctx := context.Background()
2619

2720
cfg, err := config.NewConfig()
@@ -30,23 +23,20 @@ func main() {
3023
os.Exit(1)
3124
}
3225

33-
appInst, err = app.NewApp(ctx, cfg, logger)
26+
appInst, err := app.NewApp(ctx, cfg, logger)
3427
if err != nil {
3528
logger.Error("app init failed", slog.String("error", err.Error()))
3629
os.Exit(1)
3730
}
3831

39-
mux := http.NewServeMux()
40-
mux.HandleFunc("/", httpHandler)
41-
4232
port := os.Getenv("APP_PORT")
4333
if port == "" {
4434
port = "8080"
4535
}
4636

4737
srv := &http.Server{
4838
Addr: ":" + port,
49-
Handler: mux,
39+
Handler: appInst.Handler(),
5040
ReadTimeout: 15 * time.Second,
5141
WriteTimeout: 15 * time.Second,
5242
IdleTimeout: 60 * time.Second,
@@ -80,42 +70,3 @@ func main() {
8070
<-done
8171
logger.Info("server stopped")
8272
}
83-
84-
// httpHandler converts http.Request to app.Request and handles the response.
85-
func httpHandler(w http.ResponseWriter, r *http.Request) {
86-
defer r.Body.Close()
87-
body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // 10MB limit
88-
if err != nil {
89-
http.Error(w, "failed to read request body", http.StatusBadRequest)
90-
return
91-
}
92-
93-
headers := make(map[string]string)
94-
for key, values := range r.Header {
95-
if len(values) > 0 {
96-
headers[strings.ToLower(key)] = values[0]
97-
}
98-
}
99-
100-
req := app.Request{
101-
Type: app.RequestTypeHTTP,
102-
Method: r.Method,
103-
Path: r.URL.Path,
104-
Headers: headers,
105-
Body: body,
106-
}
107-
108-
resp := appInst.HandleRequest(r.Context(), req)
109-
110-
for key, value := range resp.Headers {
111-
w.Header().Set(key, value)
112-
}
113-
if resp.ContentType != "" && w.Header().Get("Content-Type") == "" {
114-
w.Header().Set("Content-Type", resp.ContentType)
115-
}
116-
117-
w.WriteHeader(resp.StatusCode)
118-
if len(resp.Body) > 0 {
119-
w.Write(resp.Body)
120-
}
121-
}

0 commit comments

Comments
 (0)