Skip to content

Fix GHES GraphQL path handling and end-to-end query preservation in DIFC proxy when upstream is /api/v3#3970

Merged
lpcox merged 10 commits intomainfrom
copilot/fix-difc-proxy-404-error
Apr 16, 2026
Merged

Fix GHES GraphQL path handling and end-to-end query preservation in DIFC proxy when upstream is /api/v3#3970
lpcox merged 10 commits intomainfrom
copilot/fix-difc-proxy-404-error

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 16, 2026

The DIFC proxy was forwarding GraphQL requests to an invalid GHES endpoint when the upstream API base was configured as .../api/v3, causing gh CLI requests routed through the proxy (e.g. /api/graphql) to fail with 404. This change normalizes GraphQL forwarding for GHES and ensures GraphQL query strings are preserved end-to-end while keeping existing REST behavior unchanged.

  • GraphQL forwarding normalization for GHES

    • Updated proxy upstream URL construction so that when githubAPIURL ends with /api/v3, GraphQL requests are forwarded to /api/graphql instead of /api/v3/graphql.
    • GHES rewrite now applies across accepted GraphQL path forms (/graphql, /api/graphql, /api/v3/graphql).
    • Preserves query strings on GraphQL requests.
  • End-to-end query-string preservation

    • Updated GraphQL forwarding call sites to pass the original request path (including raw query) rather than a hard-coded "/graphql" path, so query parameters are not dropped in production DIFC flow.
  • Targeted coverage for rewritten GraphQL paths

    • Added/updated unit tests to verify GHES rewrite and query preservation for:
      • /graphql/api/graphql
      • /graphql?foo=bar/api/graphql?foo=bar
      • /api/graphql/api/graphql
      • /api/v3/graphql?foo=bar/api/graphql?foo=bar
    • Added handler tests to verify query-string preservation across GraphQL inbound path variants.
pathOnly, query, hasQuery := strings.Cut(path, "?")
if strings.HasSuffix(s.githubAPIURL, "/api/v3") && IsGraphQLPath(pathOnly) {
    url = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql"
    if hasQuery {
        url += "?" + query
    }
}

> [!WARNING]
>
>

@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented Apr 16, 2026

@copilot are there other potential paths that the difc proxy should support? can you document all the paths it does support and why?

Copilot AI changed the title [WIP] Fix DIFC proxy returning 404 for GHES API paths Fix GHES GraphQL path handling in DIFC proxy when upstream is /api/v3 Apr 16, 2026
Copilot AI requested a review from lpcox April 16, 2026 18:56
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot are there other potential paths that the difc proxy should support? can you document all the paths it does support and why?

I documented the current supported proxy paths and rationale in docs/PROXY_MODE.md (commit 7ecb220).

Short answer: the proxy intentionally supports the read endpoints needed for repo-assist/gh workflows (REST route table + GraphQL on /graphql, /api/graphql, /api/v3/graphql), normalizes GH_HOST /api/v3/... paths, and fails closed for unmatched read routes to avoid unfiltered data leakage. Non-read/write methods still pass through unchanged.

If you want additional specific read endpoints covered, I can add them explicitly to internal/proxy/router.go and tests.

@lpcox lpcox marked this pull request as ready for review April 16, 2026 19:07
Copilot AI review requested due to automatic review settings April 16, 2026 19:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes GHES GraphQL forwarding in the DIFC proxy when the configured upstream GitHub API base is .../api/v3, ensuring GraphQL requests are sent to the correct GHES endpoint (/api/graphql) while keeping REST behavior unchanged.

Changes:

  • Rewrites upstream URL construction for POST /graphql when githubAPIURL ends with /api/v3 to forward to /api/graphql.
  • Adds a unit test validating the /graphql/api/graphql rewrite behavior (including a query-string case).
  • Updates proxy-mode documentation to describe REST path normalization and accepted GraphQL endpoint paths, including the GHES rewrite.
Show a summary per file
File Description
internal/proxy/proxy.go Adds GHES-specific GraphQL forwarding rewrite from /graphql to /api/graphql when upstream base is /api/v3.
internal/proxy/proxy_test.go Adds coverage for the GHES GraphQL rewrite in forwardToGitHub.
docs/PROXY_MODE.md Documents REST normalization, fail-closed behavior for reads, GraphQL accepted paths, and GHES GraphQL rewrite.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment thread internal/proxy/proxy.go
Comment on lines 399 to +405
url := s.githubAPIURL + path
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
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphQL query-string preservation appears incomplete end-to-end: forwardToGitHub now preserves ?… when path includes it, but the proxy’s GraphQL forwarding path is hard-coded elsewhere as "/graphql" (without the original query), so real /graphql?foo=bar requests will still drop the query. Consider plumbing the original request’s RawQuery into the GraphQL forward path (or switching the GraphQL forwarding call sites to pass the full path argument) so this logic is actually exercised in production.

See below for a potential fix:

// path should be the original request URI path, including any raw query string.
func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) {
	baseURL, err := url.Parse(s.githubAPIURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse GitHub API URL: %w", err)
	}

	requestURL, err := url.Parse(path)
	if err != nil {
		return nil, fmt.Errorf("failed to parse upstream path %q: %w", path, err)
	}

	pathOnly := requestURL.Path
	if strings.HasSuffix(baseURL.Path, "/api/v3") && pathOnly == "/graphql" {
		baseURL.Path = strings.TrimSuffix(baseURL.Path, "/api/v3") + "/api/graphql"
		baseURL.RawQuery = requestURL.RawQuery
	} else {
		baseURL = baseURL.ResolveReference(requestURL)
	}

	targetURL := baseURL.String()
	logProxy.Printf("forwarding %s %s → %s", method, path, targetURL)

	req, err := http.NewRequestWithContext(ctx, method, targetURL, body)
	if err != nil {

Copilot uses AI. Check for mistakes.
@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented Apr 16, 2026

@copilot update the pr based on this review feedback

@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented Apr 16, 2026

@copilot resolve the merge conflicts in this pull request

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot update the pr based on this review feedback

Updated based on the review feedback in commit 271692b (plus logic fix in f35254a).

The proxy now preserves GraphQL query strings end-to-end by forwarding the original request path (including query) through both GraphQL forwarding call sites, and GHES rewrite handling now accepts /graphql, /api/graphql, and /api/v3/graphql when upstream is /api/v3. I also added/expanded tests to cover these path/query preservation scenarios.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build3690068192/b510/launcher.test /tmp/go-build3690068192/b510/launcher.test -test.testlogfile=/tmp/go-build3690068192/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true rotocol/go-sdk@v1.5.0/oauthex/auth_meta.go rotocol/go-sdk@v1.5.0/oauthex/client.go x_amd64/vet --gdwarf-5 ternal/engine/in-atomic ut-3450205222.c x_amd64/vet 6475�� _.a -trimpath x_amd64/vet -p go-sdk/auth -lang=go1.25 x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3653468039/b514/launcher.test /tmp/go-build3653468039/b514/launcher.test -test.testlogfile=/tmp/go-build3653468039/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s 6475�� /tmp/go-build908647524/b450/_pkg-c=4 .cfg 64/pkg/tool/linux_amd64/vet -p github.com/model--norc -lang=go1.25 64/pkg/tool/linux_amd64/vet 6475�� /tmp/go-build908647524/b108/vet.cfg -goversion 64/pkg/tool/linux_amd64/vet -c=4 -nolocalimports -importcfg 64/pkg/tool/linux_amd64/vet (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build3690068192/b492/config.test /tmp/go-build3690068192/b492/config.test -test.testlogfile=/tmp/go-build3690068192/b492/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true @v1.1.3/cpu/arm64/arm64.go 647524/b299/ x_amd64/vet --gdwarf-5 nal/encoding/tag-atomic -o x_amd64/vet 6475�� _.a pkg/mod/go.opent-ifaceassert x_amd64/vet -p (dns block)
    • Triggering command: /tmp/go-build3653468039/b496/config.test /tmp/go-build3653468039/b496/config.test -test.testlogfile=/tmp/go-build3653468039/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s 6475�� /tmp/go-build908647524/b246/_pkg-errorsas .cfg 64/pkg/tool/linux_amd64/vet -p github.com/tetradocker-cli-plugin-metadata -lang=go1.24 64/pkg/tool/linux_amd64/vet (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build3690068192/b510/launcher.test /tmp/go-build3690068192/b510/launcher.test -test.testlogfile=/tmp/go-build3690068192/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true rotocol/go-sdk@v1.5.0/oauthex/auth_meta.go rotocol/go-sdk@v1.5.0/oauthex/client.go x_amd64/vet --gdwarf-5 ternal/engine/in-atomic ut-3450205222.c x_amd64/vet 6475�� _.a -trimpath x_amd64/vet -p go-sdk/auth -lang=go1.25 x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3653468039/b514/launcher.test /tmp/go-build3653468039/b514/launcher.test -test.testlogfile=/tmp/go-build3653468039/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s 6475�� /tmp/go-build908647524/b450/_pkg-c=4 .cfg 64/pkg/tool/linux_amd64/vet -p github.com/model--norc -lang=go1.25 64/pkg/tool/linux_amd64/vet 6475�� /tmp/go-build908647524/b108/vet.cfg -goversion 64/pkg/tool/linux_amd64/vet -c=4 -nolocalimports -importcfg 64/pkg/tool/linux_amd64/vet (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build3690068192/b510/launcher.test /tmp/go-build3690068192/b510/launcher.test -test.testlogfile=/tmp/go-build3690068192/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true rotocol/go-sdk@v1.5.0/oauthex/auth_meta.go rotocol/go-sdk@v1.5.0/oauthex/client.go x_amd64/vet --gdwarf-5 ternal/engine/in-atomic ut-3450205222.c x_amd64/vet 6475�� _.a -trimpath x_amd64/vet -p go-sdk/auth -lang=go1.25 x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3653468039/b514/launcher.test /tmp/go-build3653468039/b514/launcher.test -test.testlogfile=/tmp/go-build3653468039/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s 6475�� /tmp/go-build908647524/b450/_pkg-c=4 .cfg 64/pkg/tool/linux_amd64/vet -p github.com/model--norc -lang=go1.25 64/pkg/tool/linux_amd64/vet 6475�� /tmp/go-build908647524/b108/vet.cfg -goversion 64/pkg/tool/linux_amd64/vet -c=4 -nolocalimports -importcfg 64/pkg/tool/linux_amd64/vet (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build3690068192/b519/mcp.test /tmp/go-build3690068192/b519/mcp.test -test.testlogfile=/tmp/go-build3690068192/b519/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 647524/b412/_pkg-s /tmp/go-build908-w x_amd64/vet 647524/b164/ t/transform --64 x_amd64/vet -I cfg -I x_amd64/vet --gdwarf-5 647524/b452/ -o x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3653468039/b523/mcp.test /tmp/go-build3653468039/b523/mcp.test -test.testlogfile=/tmp/go-build3653468039/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s 6475�� /tmp/go-build908647524/b085/vet.cfg .cfg 64/pkg/tool/linux_amd64/vet -c=4 -nolocalimports -importcfg 64/pkg/tool/linu-buildtags /tmp�� /home/REDACTED/go/-errorsas /home/REDACTED/go/-ifaceassert x_amd64/compile pb/duration.pb.gbash --local ache/go/1.25.8/x--version x_amd64/compile (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Fix GHES GraphQL path handling in DIFC proxy when upstream is /api/v3 Fix GHES GraphQL path handling and end-to-end query preservation in DIFC proxy when upstream is /api/v3 Apr 16, 2026
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@copilot resolve the merge conflicts in this pull request

Resolved merge conflicts and updated the PR with merge commit 539d64d8. Conflicts were in internal/proxy/proxy_test.go; I reconciled them to keep the GraphQL GHES rewrite/query preservation coverage while incorporating latest main changes. No UI changes in this PR.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build2559757394/b510/launcher.test /tmp/go-build2559757394/b510/launcher.test -test.testlogfile=/tmp/go-build2559757394/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a ache/go/1.25.8/x64/src/os/user x_amd64/vet 9083763/b299/ /known/structpb (dns block)
    • Triggering command: /tmp/go-build3855567136/b514/launcher.test /tmp/go-build3855567136/b514/launcher.test -test.testlogfile=/tmp/go-build3855567136/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s -uns�� /tmp/go-build2169083763/b169/vet.cfg l0IgnnYtDUbmqPCsylov/l0IgnnYtDUbmqPCsylov 64/pkg/tool/linux_amd64/compile go1.25.8 -c=4 -nolocalimports 64/pkg/tool/linux_amd64/compile (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2559757394/b492/config.test /tmp/go-build2559757394/b492/config.test -test.testlogfile=/tmp/go-build2559757394/b492/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1.80.0/balancer/roundrobin/roundrobin.go 9083763/b299/ x_amd64/vet /tmp/go-build216/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -imultiarch x86_64-linux-gnu-bool x_amd64/vet 9083�� g_.a 9083763/b164/ x_amd64/vet -p metadata -lang=go1.25 x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3855567136/b496/config.test /tmp/go-build3855567136/b496/config.test -test.testlogfile=/tmp/go-build3855567136/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s 9083�� Ujpv/RRzuZ3RbMrEJhF2FUjpv .cfg 64/pkg/tool/linux_amd64/vet /tmp/go-build216bash -c=4 -nolocalimports 64/pkg/tool/linux_amd64/vet 9083�� pkg/mod/go.opentelemetry.io/otel/exporters/otlp/-errorsas pkg/mod/go.opentelemetry.io/otel/exporters/otlp/-ifaceassert 64/pkg/tool/linux_amd64/vet go --get 64/bin/as 64/pkg/tool/linux_amd64/vet (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build2559757394/b510/launcher.test /tmp/go-build2559757394/b510/launcher.test -test.testlogfile=/tmp/go-build2559757394/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a ache/go/1.25.8/x64/src/os/user x_amd64/vet 9083763/b299/ /known/structpb (dns block)
    • Triggering command: /tmp/go-build3855567136/b514/launcher.test /tmp/go-build3855567136/b514/launcher.test -test.testlogfile=/tmp/go-build3855567136/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s -uns�� /tmp/go-build2169083763/b169/vet.cfg l0IgnnYtDUbmqPCsylov/l0IgnnYtDUbmqPCsylov 64/pkg/tool/linux_amd64/compile go1.25.8 -c=4 -nolocalimports 64/pkg/tool/linux_amd64/compile (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build2559757394/b510/launcher.test /tmp/go-build2559757394/b510/launcher.test -test.testlogfile=/tmp/go-build2559757394/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a ache/go/1.25.8/x64/src/os/user x_amd64/vet 9083763/b299/ /known/structpb (dns block)
    • Triggering command: /tmp/go-build3855567136/b514/launcher.test /tmp/go-build3855567136/b514/launcher.test -test.testlogfile=/tmp/go-build3855567136/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s -uns�� /tmp/go-build2169083763/b169/vet.cfg l0IgnnYtDUbmqPCsylov/l0IgnnYtDUbmqPCsylov 64/pkg/tool/linux_amd64/compile go1.25.8 -c=4 -nolocalimports 64/pkg/tool/linux_amd64/compile (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2559757394/b519/mcp.test /tmp/go-build2559757394/b519/mcp.test -test.testlogfile=/tmp/go-build2559757394/b519/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1.80.0/health/gr-errorsas 1.80.0/health/gr-ifaceassert x_amd64/vet /opt/hostedtoolc/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -I /tmp/go-build216-bool x_amd64/vet 9083�� /cmd/completion.-errorsas /cmd/flags.go x_amd64/vet . --gdwarf2 --64 x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3855567136/b523/mcp.test /tmp/go-build3855567136/b523/mcp.test -test.testlogfile=/tmp/go-build3855567136/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp�� (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@lpcox lpcox merged commit 410d054 into main Apr 16, 2026
14 checks passed
@lpcox lpcox deleted the copilot/fix-difc-proxy-404-error branch April 16, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: DIFC proxy returns 404 for GHES API paths (/api/graphql, /api/v3/...)

3 participants