15c7f07f
feat: load middleware config in nono-proxy main
a73x 2026-03-31 05:53
Commit message
cmd/nono-proxy/main.go
| Old | New | ||
|---|---|---|---|
| @@ -44,9 +44,12 @@ func main() { | |||
| 44 | } | 44 | } |
| 45 | log.Printf("CA cert: %s/ca.pem", store) | 45 | log.Printf("CA cert: %s/ca.pem", store) |
| 46 | 46 | ||
| 47 | mwPath := filepath.Join(store, "middleware.yaml") | ||
| 48 | |||
| 47 | opts := []proxy.Option{ | 49 | opts := []proxy.Option{ |
| 48 | proxy.WithRules(rulesPath), | 50 | proxy.WithRules(rulesPath), |
| 49 | proxy.WithCA(caCert, caKey), | 51 | proxy.WithCA(caCert, caKey), |
| 52 | proxy.WithMiddleware(mwPath), | ||
| 50 | } | 53 | } |
| 51 | 54 | ||
| 52 | p := proxy.New(hostsFile, opts...) | 55 | p := proxy.New(hostsFile, opts...) |
docs/superpowers/plans/2026-03-31-middleware.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,532 @@ | |||
| 1 | # Middleware Implementation Plan | ||
| 2 | |||
| 3 | > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | ||
| 4 | |||
| 5 | **Goal:** Add a configurable middleware system that intercepts HTTPS responses matching URL patterns and writes response bodies to files. | ||
| 6 | |||
| 7 | **Architecture:** New `middleware` package loads a YAML config mapping `host+path` patterns to `save_response` actions with a destination file. The proxy gains a `WithMiddleware` option. In the MITM loop, after reading the upstream response, middleware checks the request URL and writes the response body to the configured destination (overwriting). | ||
| 8 | |||
| 9 | **Tech Stack:** Go stdlib, `gopkg.in/yaml.v3` (already a dependency) | ||
| 10 | |||
| 11 | --- | ||
| 12 | |||
| 13 | ### Task 1: Middleware package — config loading | ||
| 14 | |||
| 15 | **Files:** | ||
| 16 | - Create: `middleware/middleware.go` | ||
| 17 | - Create: `middleware/middleware_test.go` | ||
| 18 | |||
| 19 | - [ ] **Step 1: Write the failing test for YAML loading** | ||
| 20 | |||
| 21 | In `middleware/middleware_test.go`: | ||
| 22 | |||
| 23 | ```go | ||
| 24 | package middleware_test | ||
| 25 | |||
| 26 | import ( | ||
| 27 | "os" | ||
| 28 | "path/filepath" | ||
| 29 | "testing" | ||
| 30 | |||
| 31 | "github.com/xanderle/nono/middleware" | ||
| 32 | ) | ||
| 33 | |||
| 34 | func TestNewLoadsConfig(t *testing.T) { | ||
| 35 | dir := t.TempDir() | ||
| 36 | path := filepath.Join(dir, "middleware.yaml") | ||
| 37 | os.WriteFile(path, []byte(`middleware: | ||
| 38 | - match: "api.anthropic.com/api/oauth/usage" | ||
| 39 | action: save_response | ||
| 40 | dest: "/tmp/usage.json" | ||
| 41 | `), 0644) | ||
| 42 | |||
| 43 | mw, err := middleware.New(path) | ||
| 44 | if err != nil { | ||
| 45 | t.Fatalf("unexpected error: %v", err) | ||
| 46 | } | ||
| 47 | if mw.RuleCount() != 1 { | ||
| 48 | t.Errorf("expected 1 rule, got %d", mw.RuleCount()) | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | func TestNewReturnsEmptyForMissingFile(t *testing.T) { | ||
| 53 | mw, err := middleware.New("/nonexistent/middleware.yaml") | ||
| 54 | if err != nil { | ||
| 55 | t.Fatalf("unexpected error: %v", err) | ||
| 56 | } | ||
| 57 | if mw.RuleCount() != 0 { | ||
| 58 | t.Errorf("expected 0 rules, got %d", mw.RuleCount()) | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | func TestNewRejectsUnknownAction(t *testing.T) { | ||
| 63 | dir := t.TempDir() | ||
| 64 | path := filepath.Join(dir, "middleware.yaml") | ||
| 65 | os.WriteFile(path, []byte(`middleware: | ||
| 66 | - match: "example.com/foo" | ||
| 67 | action: delete_everything | ||
| 68 | dest: "/tmp/out" | ||
| 69 | `), 0644) | ||
| 70 | |||
| 71 | _, err := middleware.New(path) | ||
| 72 | if err == nil { | ||
| 73 | t.Fatal("expected error for unknown action") | ||
| 74 | } | ||
| 75 | } | ||
| 76 | ``` | ||
| 77 | |||
| 78 | - [ ] **Step 2: Run tests to verify they fail** | ||
| 79 | |||
| 80 | Run: `go test ./middleware/...` | ||
| 81 | Expected: package does not exist | ||
| 82 | |||
| 83 | - [ ] **Step 3: Implement middleware package** | ||
| 84 | |||
| 85 | In `middleware/middleware.go`: | ||
| 86 | |||
| 87 | ```go | ||
| 88 | package middleware | ||
| 89 | |||
| 90 | import ( | ||
| 91 | "fmt" | ||
| 92 | "os" | ||
| 93 | |||
| 94 | "gopkg.in/yaml.v3" | ||
| 95 | ) | ||
| 96 | |||
| 97 | type Rule struct { | ||
| 98 | Match string | ||
| 99 | Action string | ||
| 100 | Dest string | ||
| 101 | } | ||
| 102 | |||
| 103 | type Middleware struct { | ||
| 104 | rules []Rule | ||
| 105 | } | ||
| 106 | |||
| 107 | type yamlRule struct { | ||
| 108 | Match string `yaml:"match"` | ||
| 109 | Action string `yaml:"action"` | ||
| 110 | Dest string `yaml:"dest"` | ||
| 111 | } | ||
| 112 | |||
| 113 | type yamlConfig struct { | ||
| 114 | Middleware []yamlRule `yaml:"middleware"` | ||
| 115 | } | ||
| 116 | |||
| 117 | // New loads middleware config from a YAML file. | ||
| 118 | // Returns an empty Middleware if the file does not exist. | ||
| 119 | func New(path string) (*Middleware, error) { | ||
| 120 | data, err := os.ReadFile(path) | ||
| 121 | if err != nil { | ||
| 122 | if os.IsNotExist(err) { | ||
| 123 | return &Middleware{}, nil | ||
| 124 | } | ||
| 125 | return nil, fmt.Errorf("reading middleware config: %w", err) | ||
| 126 | } | ||
| 127 | |||
| 128 | var cfg yamlConfig | ||
| 129 | if err := yaml.Unmarshal(data, &cfg); err != nil { | ||
| 130 | return nil, fmt.Errorf("parsing middleware YAML: %w", err) | ||
| 131 | } | ||
| 132 | |||
| 133 | rules := make([]Rule, 0, len(cfg.Middleware)) | ||
| 134 | for _, yr := range cfg.Middleware { | ||
| 135 | if yr.Action != "save_response" { | ||
| 136 | return nil, fmt.Errorf("unknown middleware action %q for match %q", yr.Action, yr.Match) | ||
| 137 | } | ||
| 138 | rules = append(rules, Rule{Match: yr.Match, Action: yr.Action, Dest: yr.Dest}) | ||
| 139 | } | ||
| 140 | |||
| 141 | return &Middleware{rules: rules}, nil | ||
| 142 | } | ||
| 143 | |||
| 144 | func (m *Middleware) RuleCount() int { | ||
| 145 | return len(m.rules) | ||
| 146 | } | ||
| 147 | ``` | ||
| 148 | |||
| 149 | - [ ] **Step 4: Run tests to verify they pass** | ||
| 150 | |||
| 151 | Run: `go test ./middleware/...` | ||
| 152 | Expected: PASS | ||
| 153 | |||
| 154 | - [ ] **Step 5: Commit** | ||
| 155 | |||
| 156 | ```bash | ||
| 157 | git add middleware/ | ||
| 158 | git commit -m "feat: add middleware package with YAML config loading" | ||
| 159 | ``` | ||
| 160 | |||
| 161 | --- | ||
| 162 | |||
| 163 | ### Task 2: Middleware — match and save logic | ||
| 164 | |||
| 165 | **Files:** | ||
| 166 | - Modify: `middleware/middleware.go` | ||
| 167 | - Modify: `middleware/middleware_test.go` | ||
| 168 | |||
| 169 | - [ ] **Step 1: Write failing test for Match** | ||
| 170 | |||
| 171 | Append to `middleware/middleware_test.go`: | ||
| 172 | |||
| 173 | ```go | ||
| 174 | func TestMatchReturnsRuleForMatchingURL(t *testing.T) { | ||
| 175 | dir := t.TempDir() | ||
| 176 | path := filepath.Join(dir, "middleware.yaml") | ||
| 177 | os.WriteFile(path, []byte(`middleware: | ||
| 178 | - match: "api.anthropic.com/api/oauth/usage" | ||
| 179 | action: save_response | ||
| 180 | dest: "/tmp/usage.json" | ||
| 181 | `), 0644) | ||
| 182 | |||
| 183 | mw, _ := middleware.New(path) | ||
| 184 | rule := mw.Match("api.anthropic.com", "/api/oauth/usage") | ||
| 185 | if rule == nil { | ||
| 186 | t.Fatal("expected a match") | ||
| 187 | } | ||
| 188 | if rule.Dest != "/tmp/usage.json" { | ||
| 189 | t.Errorf("expected dest /tmp/usage.json, got %s", rule.Dest) | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | func TestMatchReturnsNilForNoMatch(t *testing.T) { | ||
| 194 | dir := t.TempDir() | ||
| 195 | path := filepath.Join(dir, "middleware.yaml") | ||
| 196 | os.WriteFile(path, []byte(`middleware: | ||
| 197 | - match: "api.anthropic.com/api/oauth/usage" | ||
| 198 | action: save_response | ||
| 199 | dest: "/tmp/usage.json" | ||
| 200 | `), 0644) | ||
| 201 | |||
| 202 | mw, _ := middleware.New(path) | ||
| 203 | rule := mw.Match("example.com", "/other") | ||
| 204 | if rule != nil { | ||
| 205 | t.Fatal("expected no match") | ||
| 206 | } | ||
| 207 | } | ||
| 208 | ``` | ||
| 209 | |||
| 210 | - [ ] **Step 2: Run tests to verify they fail** | ||
| 211 | |||
| 212 | Run: `go test ./middleware/...` | ||
| 213 | Expected: `mw.Match` undefined | ||
| 214 | |||
| 215 | - [ ] **Step 3: Implement Match** | ||
| 216 | |||
| 217 | Add to `middleware/middleware.go`: | ||
| 218 | |||
| 219 | ```go | ||
| 220 | // Match checks if host+path matches any middleware rule. | ||
| 221 | // Returns the first matching rule, or nil. | ||
| 222 | func (m *Middleware) Match(host, path string) *Rule { | ||
| 223 | url := host + path | ||
| 224 | for i := range m.rules { | ||
| 225 | if m.rules[i].Match == url { | ||
| 226 | return &m.rules[i] | ||
| 227 | } | ||
| 228 | } | ||
| 229 | return nil | ||
| 230 | } | ||
| 231 | ``` | ||
| 232 | |||
| 233 | - [ ] **Step 4: Run tests to verify they pass** | ||
| 234 | |||
| 235 | Run: `go test ./middleware/...` | ||
| 236 | Expected: PASS | ||
| 237 | |||
| 238 | - [ ] **Step 5: Write failing test for SaveResponse** | ||
| 239 | |||
| 240 | Append to `middleware/middleware_test.go`: | ||
| 241 | |||
| 242 | ```go | ||
| 243 | func TestSaveResponseWritesBodyToFile(t *testing.T) { | ||
| 244 | dest := filepath.Join(t.TempDir(), "out.json") | ||
| 245 | rule := &middleware.Rule{ | ||
| 246 | Match: "example.com/data", | ||
| 247 | Action: "save_response", | ||
| 248 | Dest: dest, | ||
| 249 | } | ||
| 250 | |||
| 251 | body := []byte(`{"tokens": 42}`) | ||
| 252 | err := rule.SaveResponse(body) | ||
| 253 | if err != nil { | ||
| 254 | t.Fatalf("unexpected error: %v", err) | ||
| 255 | } | ||
| 256 | |||
| 257 | got, err := os.ReadFile(dest) | ||
| 258 | if err != nil { | ||
| 259 | t.Fatalf("failed to read dest file: %v", err) | ||
| 260 | } | ||
| 261 | if string(got) != string(body) { | ||
| 262 | t.Errorf("expected %q, got %q", body, got) | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | func TestSaveResponseOverwritesExistingFile(t *testing.T) { | ||
| 267 | dest := filepath.Join(t.TempDir(), "out.json") | ||
| 268 | os.WriteFile(dest, []byte("old data"), 0644) | ||
| 269 | |||
| 270 | rule := &middleware.Rule{ | ||
| 271 | Match: "example.com/data", | ||
| 272 | Action: "save_response", | ||
| 273 | Dest: dest, | ||
| 274 | } | ||
| 275 | |||
| 276 | body := []byte(`{"new": true}`) | ||
| 277 | rule.SaveResponse(body) | ||
| 278 | |||
| 279 | got, _ := os.ReadFile(dest) | ||
| 280 | if string(got) != string(body) { | ||
| 281 | t.Errorf("expected %q, got %q", body, got) | ||
| 282 | } | ||
| 283 | } | ||
| 284 | ``` | ||
| 285 | |||
| 286 | - [ ] **Step 6: Run tests to verify they fail** | ||
| 287 | |||
| 288 | Run: `go test ./middleware/...` | ||
| 289 | Expected: `rule.SaveResponse` undefined | ||
| 290 | |||
| 291 | - [ ] **Step 7: Implement SaveResponse** | ||
| 292 | |||
| 293 | Add to `middleware/middleware.go`: | ||
| 294 | |||
| 295 | ```go | ||
| 296 | // SaveResponse writes the response body to the rule's destination file, | ||
| 297 | // overwriting any existing content. | ||
| 298 | func (r *Rule) SaveResponse(body []byte) error { | ||
| 299 | return os.WriteFile(r.Dest, body, 0644) | ||
| 300 | } | ||
| 301 | ``` | ||
| 302 | |||
| 303 | - [ ] **Step 8: Run tests to verify they pass** | ||
| 304 | |||
| 305 | Run: `go test ./middleware/...` | ||
| 306 | Expected: PASS | ||
| 307 | |||
| 308 | - [ ] **Step 9: Commit** | ||
| 309 | |||
| 310 | ```bash | ||
| 311 | git add middleware/ | ||
| 312 | git commit -m "feat: add Match and SaveResponse to middleware" | ||
| 313 | ``` | ||
| 314 | |||
| 315 | --- | ||
| 316 | |||
| 317 | ### Task 3: Wire middleware into proxy | ||
| 318 | |||
| 319 | **Files:** | ||
| 320 | - Modify: `proxy/proxy.go` | ||
| 321 | - Modify: `proxy/proxy_test.go` | ||
| 322 | |||
| 323 | - [ ] **Step 1: Write failing test for middleware in MITM path** | ||
| 324 | |||
| 325 | Append to `proxy/proxy_test.go`: | ||
| 326 | |||
| 327 | ```go | ||
| 328 | func TestMiddlewareSavesResponseBody(t *testing.T) { | ||
| 329 | responseBody := `{"usage": 100}` | ||
| 330 | backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 331 | w.Header().Set("Content-Type", "application/json") | ||
| 332 | w.WriteHeader(http.StatusOK) | ||
| 333 | w.Write([]byte(responseBody)) | ||
| 334 | })) | ||
| 335 | defer backend.Close() | ||
| 336 | |||
| 337 | backendURL, _ := url.Parse(backend.URL) | ||
| 338 | host := backendURL.Hostname() | ||
| 339 | |||
| 340 | hostsFile := filepath.Join(t.TempDir(), "approved_hosts") | ||
| 341 | os.WriteFile(hostsFile, []byte(host+"\n"), 0644) | ||
| 342 | |||
| 343 | destFile := filepath.Join(t.TempDir(), "usage.json") | ||
| 344 | mwPath := filepath.Join(t.TempDir(), "middleware.yaml") | ||
| 345 | os.WriteFile(mwPath, []byte(fmt.Sprintf(`middleware: | ||
| 346 | - match: "%s/data" | ||
| 347 | action: save_response | ||
| 348 | dest: "%s" | ||
| 349 | `, host, destFile)), 0644) | ||
| 350 | |||
| 351 | caDir := t.TempDir() | ||
| 352 | caCert, caKey, err := nca.LoadOrCreate(caDir) | ||
| 353 | if err != nil { | ||
| 354 | t.Fatalf("CA setup: %v", err) | ||
| 355 | } | ||
| 356 | |||
| 357 | p := proxy.New(hostsFile, | ||
| 358 | proxy.WithCA(caCert, caKey), | ||
| 359 | proxy.WithUpstreamTLS(&tls.Config{InsecureSkipVerify: true}), | ||
| 360 | proxy.WithMiddleware(mwPath), | ||
| 361 | ) | ||
| 362 | srv := httptest.NewServer(p) | ||
| 363 | defer srv.Close() | ||
| 364 | |||
| 365 | caPool := x509.NewCertPool() | ||
| 366 | caPool.AddCert(caCert) | ||
| 367 | |||
| 368 | proxyURL, _ := url.Parse(srv.URL) | ||
| 369 | client := &http.Client{ | ||
| 370 | Transport: &http.Transport{ | ||
| 371 | Proxy: http.ProxyURL(proxyURL), | ||
| 372 | TLSClientConfig: &tls.Config{ | ||
| 373 | RootCAs: caPool, | ||
| 374 | }, | ||
| 375 | }, | ||
| 376 | } | ||
| 377 | |||
| 378 | resp, err := client.Get(backend.URL + "/data") | ||
| 379 | if err != nil { | ||
| 380 | t.Fatalf("unexpected error: %v", err) | ||
| 381 | } | ||
| 382 | defer resp.Body.Close() | ||
| 383 | |||
| 384 | if resp.StatusCode != http.StatusOK { | ||
| 385 | t.Errorf("expected 200, got %d", resp.StatusCode) | ||
| 386 | } | ||
| 387 | |||
| 388 | got, err := os.ReadFile(destFile) | ||
| 389 | if err != nil { | ||
| 390 | t.Fatalf("dest file not written: %v", err) | ||
| 391 | } | ||
| 392 | if string(got) != responseBody { | ||
| 393 | t.Errorf("expected %q, got %q", responseBody, got) | ||
| 394 | } | ||
| 395 | } | ||
| 396 | ``` | ||
| 397 | |||
| 398 | Note: add `"fmt"` to the imports block in `proxy_test.go`. | ||
| 399 | |||
| 400 | - [ ] **Step 2: Run tests to verify they fail** | ||
| 401 | |||
| 402 | Run: `go test ./proxy/...` | ||
| 403 | Expected: `proxy.WithMiddleware` undefined | ||
| 404 | |||
| 405 | - [ ] **Step 3: Add WithMiddleware option and wire into MITM loop** | ||
| 406 | |||
| 407 | In `proxy/proxy.go`, add the `middleware` import: | ||
| 408 | |||
| 409 | ```go | ||
| 410 | import ( | ||
| 411 | // ... existing imports ... | ||
| 412 | "github.com/xanderle/nono/middleware" | ||
| 413 | ) | ||
| 414 | ``` | ||
| 415 | |||
| 416 | Add the field to the `Proxy` struct: | ||
| 417 | |||
| 418 | ```go | ||
| 419 | type Proxy struct { | ||
| 420 | hostsFile string | ||
| 421 | scanner *scanner.Scanner | ||
| 422 | middleware *middleware.Middleware | ||
| 423 | caCert *x509.Certificate | ||
| 424 | // ... rest unchanged ... | ||
| 425 | } | ||
| 426 | ``` | ||
| 427 | |||
| 428 | Add the option constructor: | ||
| 429 | |||
| 430 | ```go | ||
| 431 | // WithMiddleware returns an Option that loads middleware from the given config path. | ||
| 432 | func WithMiddleware(path string) Option { | ||
| 433 | return func(p *Proxy) { | ||
| 434 | mw, err := middleware.New(path) | ||
| 435 | if err != nil { | ||
| 436 | log.Printf("WARNING: failed to load middleware from %q: %v", path, err) | ||
| 437 | return | ||
| 438 | } | ||
| 439 | p.middleware = mw | ||
| 440 | } | ||
| 441 | } | ||
| 442 | ``` | ||
| 443 | |||
| 444 | In `handleConnectMITM`, after `upstreamResp.Body.Close()` and before the `req.Close` check, add the middleware interception. Replace the response relay section: | ||
| 445 | |||
| 446 | ```go | ||
| 447 | // Replace this: | ||
| 448 | // upstreamResp.Write(tlsConn) | ||
| 449 | // upstreamResp.Body.Close() | ||
| 450 | // | ||
| 451 | // With this: | ||
| 452 | if p.middleware != nil { | ||
| 453 | if rule := p.middleware.Match(host, req.URL.Path); rule != nil { | ||
| 454 | body, err := io.ReadAll(upstreamResp.Body) | ||
| 455 | upstreamResp.Body.Close() | ||
| 456 | if err != nil { | ||
| 457 | log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err) | ||
| 458 | } else { | ||
| 459 | if err := rule.SaveResponse(body); err != nil { | ||
| 460 | log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err) | ||
| 461 | } else { | ||
| 462 | log.Printf("MIDDLEWARE saved %s%s -> %s", host, req.URL.Path, rule.Dest) | ||
| 463 | } | ||
| 464 | upstreamResp.Body = io.NopCloser(bytes.NewReader(body)) | ||
| 465 | } | ||
| 466 | upstreamResp.Write(tlsConn) | ||
| 467 | } else { | ||
| 468 | upstreamResp.Write(tlsConn) | ||
| 469 | upstreamResp.Body.Close() | ||
| 470 | } | ||
| 471 | } else { | ||
| 472 | upstreamResp.Write(tlsConn) | ||
| 473 | upstreamResp.Body.Close() | ||
| 474 | } | ||
| 475 | ``` | ||
| 476 | |||
| 477 | - [ ] **Step 4: Run tests to verify they pass** | ||
| 478 | |||
| 479 | Run: `go test ./proxy/...` | ||
| 480 | Expected: PASS | ||
| 481 | |||
| 482 | - [ ] **Step 5: Run all tests** | ||
| 483 | |||
| 484 | Run: `go test ./...` | ||
| 485 | Expected: PASS | ||
| 486 | |||
| 487 | - [ ] **Step 6: Commit** | ||
| 488 | |||
| 489 | ```bash | ||
| 490 | git add proxy/ middleware/ | ||
| 491 | git commit -m "feat: wire middleware into proxy MITM loop" | ||
| 492 | ``` | ||
| 493 | |||
| 494 | --- | ||
| 495 | |||
| 496 | ### Task 4: Wire middleware into main and add default config | ||
| 497 | |||
| 498 | **Files:** | ||
| 499 | - Modify: `cmd/nono-proxy/main.go` | ||
| 500 | |||
| 501 | - [ ] **Step 1: Add middleware loading to main** | ||
| 502 | |||
| 503 | Add `middleware` to the import path. Load middleware config from `store/middleware.yaml` and pass it as an option: | ||
| 504 | |||
| 505 | ```go | ||
| 506 | mwPath := filepath.Join(store, "middleware.yaml") | ||
| 507 | |||
| 508 | opts := []proxy.Option{ | ||
| 509 | proxy.WithRules(rulesPath), | ||
| 510 | proxy.WithCA(caCert, caKey), | ||
| 511 | proxy.WithMiddleware(mwPath), | ||
| 512 | } | ||
| 513 | ``` | ||
| 514 | |||
| 515 | No default config file writing — the middleware YAML is opt-in. If the file doesn't exist, `middleware.New` returns an empty middleware (no-op). | ||
| 516 | |||
| 517 | - [ ] **Step 2: Build and verify** | ||
| 518 | |||
| 519 | Run: `make build` | ||
| 520 | Expected: builds successfully | ||
| 521 | |||
| 522 | - [ ] **Step 3: Run all tests** | ||
| 523 | |||
| 524 | Run: `make test` | ||
| 525 | Expected: PASS | ||
| 526 | |||
| 527 | - [ ] **Step 4: Commit** | ||
| 528 | |||
| 529 | ```bash | ||
| 530 | git add cmd/nono-proxy/main.go | ||
| 531 | git commit -m "feat: load middleware config in nono-proxy main" | ||
| 532 | ``` | ||
proxy/proxy.go
| Old | New | ||
|---|---|---|---|
| @@ -252,6 +252,7 @@ func (p *Proxy) handleConnectMITM(w http.ResponseWriter, r *http.Request) { | |||
| 252 | upstreamResp.Body.Close() | 252 | upstreamResp.Body.Close() |
| 253 | if err != nil { | 253 | if err != nil { |
| 254 | log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err) | 254 | log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err) |
| 255 | upstreamResp.Body = io.NopCloser(bytes.NewReader(nil)) | ||
| 255 | } else { | 256 | } else { |
| 256 | if err := rule.SaveResponse(body); err != nil { | 257 | if err := rule.SaveResponse(body); err != nil { |
| 257 | log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err) | 258 | log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err) |