buildHTTPRequest constructs the in-process request, attaching the user via the exported server.WithCurrentUser helper so the handler chain treats the call as authenticated. Pulled out so tests can inspect / decorate it cheaply.
(ctx context.Context, method, urlPath string, body []byte, user *models.User)
| 581 | // chain treats the call as authenticated. Pulled out so tests can |
| 582 | // inspect / decorate it cheaply. |
| 583 | func buildHTTPRequest(ctx context.Context, method, urlPath string, body []byte, user *models.User) (*http.Request, error) { |
| 584 | // Strip any inherited chi.RouteCtxKey from the inbound context |
| 585 | // before synthesizing the new request. Without this, every |
| 586 | // production /mcp tool call 404s on the synthesized /api/v1/... |
| 587 | // request because chi's Mux.ServeHTTP short-circuits when it |
| 588 | // detects an existing RouteCtxKey: |
| 589 | // |
| 590 | // // chi/v5/mux.go:71-75 |
| 591 | // rctx, _ := r.Context().Value(RouteCtxKey).(*Context) |
| 592 | // if rctx != nil { |
| 593 | // mx.handler.ServeHTTP(w, r) // bypass fresh routing |
| 594 | // return |
| 595 | // } |
| 596 | // |
| 597 | // chi assumes "if there's already a route context, I'm being |
| 598 | // invoked as a sub-router from a parent — don't reset state." |
| 599 | // That's correct for chi's own Sub() / Mount() patterns, but |
| 600 | // here we're synthesizing a brand-new request that needs to |
| 601 | // route from scratch against the ROOT mux. The stale RouteCtxKey |
| 602 | // from the inbound /mcp request causes chi to skip its |
| 603 | // rctx.Reset() + RoutePath = "/api/v1/..." setup; the route |
| 604 | // table lookup runs against contaminated routing state and |
| 605 | // falls through to chi's default NotFound handler — whose body |
| 606 | // is the literal "404 page not found\n" the production user |
| 607 | // reported on every dispatcher call. |
| 608 | // |
| 609 | // In tests this never fired because Dispatch was always called |
| 610 | // with context.Background() (no RouteCtxKey to inherit). In |
| 611 | // production every call enters via /mcp's chi-routed handler, |
| 612 | // so the contamination is universal. |
| 613 | // |
| 614 | // Setting the value to a typed nil shadows the parent's value: |
| 615 | // chi's `.(*Context)` type assertion on a context.Value of nil |
| 616 | // returns (nil, false), the `rctx != nil` check fails, and |
| 617 | // chi takes the fresh-routing branch as intended. |
| 618 | // |
| 619 | // Critically we DON'T strip pad's own context values |
| 620 | // (WithCurrentUser, WithAPITokenAuth, TokenScopes, |
| 621 | // TokenAllowedWorkspaces) — those are added below / preserved |
| 622 | // from the inbound request and are exactly what the synthesized |
| 623 | // request needs to authenticate as the same user. We only strip |
| 624 | // the chi-specific routing key. |
| 625 | ctx = context.WithValue(ctx, chi.RouteCtxKey, (*chi.Context)(nil)) |
| 626 | |
| 627 | var bodyReader io.Reader |
| 628 | if len(body) > 0 { |
| 629 | bodyReader = bytes.NewReader(body) |
| 630 | } |
| 631 | req, err := http.NewRequestWithContext(ctx, method, urlPath, bodyReader) |
| 632 | if err != nil { |
| 633 | return nil, err |
| 634 | } |
| 635 | if len(body) > 0 { |
| 636 | req.Header.Set("Content-Type", "application/json") |
| 637 | } |
| 638 | // Loopback-equivalent — handlers gating on RemoteAddr (e.g. the |
| 639 | // localhost-bootstrap path) treat this as in-process. The auth |
| 640 | // chain sees us as already-authenticated via WithCurrentUser, so |