From 587769d91d0958c4b1cc5ca3679f81aff2348ee7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:44:41 +0000 Subject: [PATCH 1/9] Initial plan From 702ddf0950023ce205eb2bbfdba7776f59c19e9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:48:58 +0000 Subject: [PATCH 2/9] fix(proxy): rewrite GraphQL upstream path for GHES api/v3 base Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/99a9af23-d3aa-4922-b49f-0cc207cab3aa Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/proxy.go | 8 ++++++++ internal/proxy/proxy_test.go | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index bdd643c3..b0ba91bb 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -397,6 +397,14 @@ func (r *restBackendCaller) CallTool(ctx context.Context, toolName string, args // if non-empty it is forwarded as-is, otherwise the configured fallback token is used. func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) { url := s.githubAPIURL + path + if strings.HasSuffix(s.githubAPIURL, "/api/v3") { + switch { + case path == "/graphql": + url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" + case strings.HasPrefix(path, "/graphql?"): + url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" + strings.TrimPrefix(path, "/graphql") + } + } logProxy.Printf("forwarding %s %s → %s", method, path, url) req, err := http.NewRequestWithContext(ctx, method, url, body) diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 56d2b8a3..364665a1 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -1,6 +1,7 @@ package proxy import ( + "context" "io" "net/http" "net/http/httptest" @@ -969,3 +970,24 @@ func TestDeriveGitHubAPIURL(t *testing.T) { }) } } + +func TestForwardToGitHub_RewritesGraphQLPathForGHESAPIBase(t *testing.T) { + var receivedPath string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedPath = r.URL.Path + w.WriteHeader(http.StatusOK) + })) + defer upstream.Close() + + s := &Server{ + githubAPIURL: upstream.URL + "/api/v3", + httpClient: upstream.Client(), + } + + resp, err := s.forwardToGitHub(context.Background(), http.MethodPost, "/graphql", nil, "application/json", "") + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, "/api/graphql", receivedPath) +} From a27bb2900f42bae8824b194b674c82162f8ba5d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:55:16 +0000 Subject: [PATCH 3/9] test(proxy): cover GHES GraphQL URL rewrite including querystring Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/99a9af23-d3aa-4922-b49f-0cc207cab3aa Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/proxy.go | 11 +++++------ internal/proxy/proxy_test.go | 30 +++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index b0ba91bb..8c942944 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -397,12 +397,11 @@ func (r *restBackendCaller) CallTool(ctx context.Context, toolName string, args // if non-empty it is forwarded as-is, otherwise the configured fallback token is used. func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) { url := s.githubAPIURL + path - if strings.HasSuffix(s.githubAPIURL, "/api/v3") { - switch { - case path == "/graphql": - url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" - case strings.HasPrefix(path, "/graphql?"): - url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" + strings.TrimPrefix(path, "/graphql") + pathOnly, query, hasQuery := strings.Cut(path, "?") + if strings.HasSuffix(s.githubAPIURL, "/api/v3") && pathOnly == "/graphql" { + url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" + if hasQuery { + url += "?" + query } } logProxy.Printf("forwarding %s %s → %s", method, path, url) diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 364665a1..747f13f3 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -973,8 +973,10 @@ func TestDeriveGitHubAPIURL(t *testing.T) { func TestForwardToGitHub_RewritesGraphQLPathForGHESAPIBase(t *testing.T) { var receivedPath string + var receivedQuery string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedPath = r.URL.Path + receivedQuery = r.URL.RawQuery w.WriteHeader(http.StatusOK) })) defer upstream.Close() @@ -984,10 +986,28 @@ func TestForwardToGitHub_RewritesGraphQLPathForGHESAPIBase(t *testing.T) { httpClient: upstream.Client(), } - resp, err := s.forwardToGitHub(context.Background(), http.MethodPost, "/graphql", nil, "application/json", "") - require.NoError(t, err) - require.NotNil(t, resp) - defer resp.Body.Close() + tests := []struct { + name string + path string + wantPath string + wantQuery string + }{ + {name: "plain graphql endpoint", path: "/graphql", wantPath: "/api/graphql"}, + {name: "graphql endpoint with query string", path: "/graphql?foo=bar", wantPath: "/api/graphql", wantQuery: "foo=bar"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + receivedPath = "" + receivedQuery = "" + + resp, err := s.forwardToGitHub(context.Background(), http.MethodPost, tt.path, nil, "application/json", "") + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() - assert.Equal(t, "/api/graphql", receivedPath) + assert.Equal(t, tt.wantPath, receivedPath) + assert.Equal(t, tt.wantQuery, receivedQuery) + }) + } } From 7ecb220b6e258a246f46303c5e5e8d772c7710be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:57:43 +0000 Subject: [PATCH 4/9] docs(proxy): document supported path formats and fail-closed routing Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/a3d9899a-0f54-47a9-b6ad-5846ecf1427e Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/PROXY_MODE.md | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/PROXY_MODE.md b/docs/PROXY_MODE.md index 1a56a2fa..ab04a9c2 100644 --- a/docs/PROXY_MODE.md +++ b/docs/PROXY_MODE.md @@ -89,14 +89,19 @@ The proxy reuses the same 6-phase pipeline as the MCP gateway, with Phase 3 adap ## REST Route Mapping -The proxy maps ~25 GitHub REST API URL patterns to guard tool names: +The proxy maps REST API URL patterns to guard tool names (see `internal/proxy/router.go` for the exact source of truth). Inbound paths are normalized first: + +- `GH_HOST` style REST paths with `/api/v3/...` are normalized to `/...` for routing. +- Query strings are ignored for route matching and still forwarded upstream. + +Supported path families include: | URL Pattern | Guard Tool | |-------------|-----------| | `/repos/:owner/:repo/issues` | `list_issues` | -| `/repos/:owner/:repo/issues/:number` | `get_issue` | +| `/repos/:owner/:repo/issues/:number` | `issue_read` | | `/repos/:owner/:repo/pulls` | `list_pull_requests` | -| `/repos/:owner/:repo/pulls/:number` | `get_pull_request` | +| `/repos/:owner/:repo/pulls/:number` | `pull_request_read` | | `/repos/:owner/:repo/commits` | `list_commits` | | `/repos/:owner/:repo/commits/:sha` | `get_commit` | | `/repos/:owner/:repo/contents/:path` | `get_file_contents` | @@ -106,21 +111,34 @@ The proxy maps ~25 GitHub REST API URL patterns to guard tool names: | `/search/code` | `search_code` | | `/search/repositories` | `search_repositories` | | `/user` | `get_me` | -| ... | See `internal/proxy/router.go` for full list | +| `/notifications` | `list_notifications` | +| `/orgs/:owner/actions/(secrets|variables)[/:name]` | `actions_list` | +| `/repos/:owner/:repo/discussions...` | `list_discussions` / `get_discussion_comments` | +| `/repos/:owner/:repo/...` (fallback) | `get_file_contents` | +| ... | See `internal/proxy/router.go` for the complete regex list and precedence | -Unrecognized URLs pass through without DIFC filtering. +For **read operations** (GET and GraphQL POST), unmatched routes are denied (fail-closed) to avoid accidental unfiltered data exposure. For **write operations** (non-read methods), requests pass through unchanged. ## GraphQL Support -GraphQL queries to `/graphql` are parsed to extract the operation type and owner/repo context: +Inbound GraphQL endpoint paths accepted by the proxy: + +- `/graphql` (github.com style) +- `/api/graphql` (GHES style used by `gh` when host is GHES/proxy) +- `/api/v3/graphql` (GH_HOST prefix style; normalized) + +GraphQL queries are parsed to extract operation type and owner/repo context: - **Repository-scoped queries** (issues, PRs, commits) — mapped to corresponding tool names - **Search queries** — mapped to `search_issues` or `search_code` - **Viewer queries** — mapped to `get_me` -- **Unknown queries** — passed through without filtering +- **Schema introspection (`__schema`, `__type`)** — passed through (safe metadata) +- **Unknown queries** — denied (fail-closed) Owner and repo are extracted from GraphQL variables (`$owner`, `$name`/`$repo`) or inline string arguments. +When the upstream API base is GHES-style `.../api/v3`, GraphQL forwarding is rewritten to `.../api/graphql` to match GHES routing. + ## Policy Notes - **Repo names must be lowercase** in policies (e.g., `octocat/hello-world` not `octocat/Hello-World`). The guard performs case-insensitive matching against actual GitHub data. From f35254a46f6c2ff92f2f64a854f89d7e2aacbf7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:22:37 +0000 Subject: [PATCH 5/9] fix(proxy): preserve GraphQL query string through DIFC forwarding Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/4809eb86-df75-49ee-94fe-225e87c3467d Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/handler.go | 4 ++-- internal/proxy/handler_test.go | 25 +++++++++++++++++++++++++ internal/proxy/proxy.go | 2 +- internal/proxy/proxy_test.go | 2 ++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/internal/proxy/handler.go b/internal/proxy/handler.go index 956c9b2c..94bc3678 100644 --- a/internal/proxy/handler.go +++ b/internal/proxy/handler.go @@ -101,7 +101,7 @@ func (h *proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if match.ToolName == "graphql_introspection" { logHandler.Printf("GraphQL introspection query, passing through") clientAuth := r.Header.Get("Authorization") - resp, respBody := h.forwardAndReadBody(w, r.Context(), http.MethodPost, "/graphql", bytes.NewReader(graphQLBody), "application/json", clientAuth) + resp, respBody := h.forwardAndReadBody(w, r.Context(), http.MethodPost, fullPath, bytes.NewReader(graphQLBody), "application/json", clientAuth) if resp == nil { return } @@ -206,7 +206,7 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa oteltrace.WithSpanKind(oteltrace.SpanKindClient), ) if graphQLBody != nil { - resp, respBody = h.forwardAndReadBody(w, fwdCtx, http.MethodPost, "/graphql", bytes.NewReader(graphQLBody), "application/json", clientAuth) + resp, respBody = h.forwardAndReadBody(w, fwdCtx, http.MethodPost, path, bytes.NewReader(graphQLBody), "application/json", clientAuth) } else { resp, respBody = h.forwardAndReadBody(w, fwdCtx, r.Method, path, nil, "", clientAuth) } diff --git a/internal/proxy/handler_test.go b/internal/proxy/handler_test.go index 76b3d219..2f730407 100644 --- a/internal/proxy/handler_test.go +++ b/internal/proxy/handler_test.go @@ -188,6 +188,31 @@ func TestServeHTTP_GraphQLIntrospectionPassthrough(t *testing.T) { assert.Contains(t, w.Body.String(), "__schema") } +func TestServeHTTP_GraphQLQueryStringForwardedToUpstream(t *testing.T) { + var receivedURL string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedURL = r.URL.RequestURI() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[]}}}}`)) //nolint:errcheck + })) + defer upstream.Close() + + s := newTestServer(t, upstream.URL) + h := &proxyHandler{server: s} + + gqlBody, _ := json.Marshal(map[string]interface{}{ + "query": `{ repository(owner:"org", name:"repo") { issues(first: 10) { nodes { id } } } }`, + }) + req := httptest.NewRequest(http.MethodPost, "/graphql?foo=bar", bytes.NewReader(gqlBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, receivedURL, "foo=bar") +} + // ─── ServeHTTP: query string is forwarded on REST GET ──────────────────────── func TestServeHTTP_QueryStringForwardedToUpstream(t *testing.T) { diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 8c942944..ddce0874 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -398,7 +398,7 @@ func (r *restBackendCaller) CallTool(ctx context.Context, toolName string, args func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) { url := s.githubAPIURL + path pathOnly, query, hasQuery := strings.Cut(path, "?") - if strings.HasSuffix(s.githubAPIURL, "/api/v3") && pathOnly == "/graphql" { + if strings.HasSuffix(s.githubAPIURL, "/api/v3") && IsGraphQLPath(pathOnly) { url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql" if hasQuery { url += "?" + query diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 747f13f3..ca82e86a 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -994,6 +994,8 @@ func TestForwardToGitHub_RewritesGraphQLPathForGHESAPIBase(t *testing.T) { }{ {name: "plain graphql endpoint", path: "/graphql", wantPath: "/api/graphql"}, {name: "graphql endpoint with query string", path: "/graphql?foo=bar", wantPath: "/api/graphql", wantQuery: "foo=bar"}, + {name: "ghes api graphql endpoint", path: "/api/graphql", wantPath: "/api/graphql"}, + {name: "gh host prefixed graphql endpoint with query string", path: "/api/v3/graphql?foo=bar", wantPath: "/api/graphql", wantQuery: "foo=bar"}, } for _, tt := range tests { From be9e218aff55f765bf76d769a9d3eabfa7b9af3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:24:19 +0000 Subject: [PATCH 6/9] test(proxy): handle write errors in graphql query forwarding test Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/4809eb86-df75-49ee-94fe-225e87c3467d Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/handler_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/proxy/handler_test.go b/internal/proxy/handler_test.go index 2f730407..c33ac19e 100644 --- a/internal/proxy/handler_test.go +++ b/internal/proxy/handler_test.go @@ -194,7 +194,8 @@ func TestServeHTTP_GraphQLQueryStringForwardedToUpstream(t *testing.T) { receivedURL = r.URL.RequestURI() w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[]}}}}`)) //nolint:errcheck + _, err := w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[]}}}}`)) + require.NoError(t, err) })) defer upstream.Close() From 6e538d921df98193473ac6f2e96007936f63822f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:26:02 +0000 Subject: [PATCH 7/9] test(proxy): assert exact graphql query forwarding URI Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/4809eb86-df75-49ee-94fe-225e87c3467d Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/proxy/handler_test.go b/internal/proxy/handler_test.go index c33ac19e..8b722979 100644 --- a/internal/proxy/handler_test.go +++ b/internal/proxy/handler_test.go @@ -211,7 +211,7 @@ func TestServeHTTP_GraphQLQueryStringForwardedToUpstream(t *testing.T) { h.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, receivedURL, "foo=bar") + assert.Equal(t, "/graphql?foo=bar", receivedURL) } // ─── ServeHTTP: query string is forwarded on REST GET ──────────────────────── From d498614d4504837ab427b7023ec2db29018c1fa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:27:41 +0000 Subject: [PATCH 8/9] test(proxy): clean up graphql query preservation test Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/4809eb86-df75-49ee-94fe-225e87c3467d Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/handler_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/proxy/handler_test.go b/internal/proxy/handler_test.go index 8b722979..200b4944 100644 --- a/internal/proxy/handler_test.go +++ b/internal/proxy/handler_test.go @@ -188,7 +188,7 @@ func TestServeHTTP_GraphQLIntrospectionPassthrough(t *testing.T) { assert.Contains(t, w.Body.String(), "__schema") } -func TestServeHTTP_GraphQLQueryStringForwardedToUpstream(t *testing.T) { +func TestServeHTTP_GraphQLPreservesQueryString(t *testing.T) { var receivedURL string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedURL = r.URL.RequestURI() @@ -202,9 +202,10 @@ func TestServeHTTP_GraphQLQueryStringForwardedToUpstream(t *testing.T) { s := newTestServer(t, upstream.URL) h := &proxyHandler{server: s} - gqlBody, _ := json.Marshal(map[string]interface{}{ + gqlBody, err := json.Marshal(map[string]interface{}{ "query": `{ repository(owner:"org", name:"repo") { issues(first: 10) { nodes { id } } } }`, }) + require.NoError(t, err) req := httptest.NewRequest(http.MethodPost, "/graphql?foo=bar", bytes.NewReader(gqlBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() From 271692b2146ed34a68754826f2eb1a21adf13e27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:29:21 +0000 Subject: [PATCH 9/9] test(proxy): cover graphql query preservation across path variants Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/4809eb86-df75-49ee-94fe-225e87c3467d Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/proxy/handler_test.go | 56 +++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/internal/proxy/handler_test.go b/internal/proxy/handler_test.go index 200b4944..ce43e420 100644 --- a/internal/proxy/handler_test.go +++ b/internal/proxy/handler_test.go @@ -189,30 +189,44 @@ func TestServeHTTP_GraphQLIntrospectionPassthrough(t *testing.T) { } func TestServeHTTP_GraphQLPreservesQueryString(t *testing.T) { - var receivedURL string - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - receivedURL = r.URL.RequestURI() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[]}}}}`)) - require.NoError(t, err) - })) - defer upstream.Close() + tests := []struct { + name string + path string + wantPath string + }{ + {name: "graphql path", path: "/graphql?foo=bar", wantPath: "/graphql?foo=bar"}, + {name: "ghes api graphql path", path: "/api/graphql?foo=bar", wantPath: "/api/graphql?foo=bar"}, + {name: "gh host prefixed graphql path", path: "/api/v3/graphql?foo=bar", wantPath: "/graphql?foo=bar"}, + } - s := newTestServer(t, upstream.URL) - h := &proxyHandler{server: s} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var receivedURL string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedURL = r.URL.RequestURI() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"data":{"repository":{"issues":{"nodes":[]}}}}`)) + require.NoError(t, err) + })) + defer upstream.Close() - gqlBody, err := json.Marshal(map[string]interface{}{ - "query": `{ repository(owner:"org", name:"repo") { issues(first: 10) { nodes { id } } } }`, - }) - require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/graphql?foo=bar", bytes.NewReader(gqlBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - h.ServeHTTP(w, req) + s := newTestServer(t, upstream.URL) + h := &proxyHandler{server: s} - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "/graphql?foo=bar", receivedURL) + gqlBody, err := json.Marshal(map[string]interface{}{ + "query": `{ repository(owner:"org", name:"repo") { issues(first: 10) { nodes { id } } } }`, + }) + require.NoError(t, err) + req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewReader(gqlBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + h.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, tt.wantPath, receivedURL) + }) + } } // ─── ServeHTTP: query string is forwarded on REST GET ────────────────────────