detached 08f0ec6

Prevent redirect errors for GitHub enterprise accounts

Replace the host for the GitHub Copilot endpoint with the
value from the token after token exchange is completed.
commit
08f0ec646f9fa1c882b548b6276effe2e1719551
author
Eckhart Koeppen · · 2026-07-13 10:36 UTC
committer
Eckhart Koeppen · · 2026-07-13 11:09 UTC
parent
1b17517
signature
unsigned
2 files · +71 -0
M packages/provider/github_copilot.go +1 -0
@@ -175,6 +175,7 @@ func (t *copilotRefreshTransport) RoundTrip(req *http.Request) (*http.Response,
175175 if u, err := url.Parse(tok.baseURL); err == nil && u.Host != clone.URL.Host {
176176 clone.URL.Scheme = u.Scheme
177177 clone.URL.Host = u.Host
178+ clone.Host = u.Host
178179 }
179180 }
180181 return t.inner.RoundTrip(clone)
@@ -175,6 +175,7 @@ func (t *copilotRefreshTransport) RoundTrip(req *http.Request) (*http.Response,
175 if u, err := url.Parse(tok.baseURL); err == nil && u.Host != clone.URL.Host {175 if u, err := url.Parse(tok.baseURL); err == nil && u.Host != clone.URL.Host {
176 clone.URL.Scheme = u.Scheme176 clone.URL.Scheme = u.Scheme
177 clone.URL.Host = u.Host177 clone.URL.Host = u.Host
178+ clone.Host = u.Host
178 }179 }
179 }180 }
180 return t.inner.RoundTrip(clone)181 return t.inner.RoundTrip(clone)
A packages/provider/github_copilot_test.go +70 -0
@@ -0,0 +1,70 @@
1+package provider
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/url"
7+ "testing"
8+ "time"
9+)
10+
11+type mockRoundTripper struct {
12+ lastReq *http.Request
13+}
14+
15+func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
16+ m.lastReq = req
17+ return &http.Response{
18+ StatusCode: http.StatusOK,
19+ Body: http.NoBody,
20+ }, nil
21+}
22+
23+func TestCopilotHostRewrite(t *testing.T) {
24+ // Seed the cache with a mock token that carries a non-default proxy endpoint
25+ // Ensure the expiresAt time is far in the future so that it bypasses the exchange check
26+ const pat = "ghp_mock_token_for_test"
27+ copilotCache.mu.Lock()
28+ copilotCache.tokens[pat] = copilotToken{
29+ value: "mock-jwt-with;proxy-ep=enterprise.copilot.proxy",
30+ expiresAt: time.Now().Add(1 * time.Hour),
31+ baseURL: "https://api.enterprise.copilot.proxy",
32+ }
33+ copilotCache.mu.Unlock()
34+
35+ mockRT := &mockRoundTripper{}
36+ transport := &copilotRefreshTransport{
37+ inner: mockRT,
38+ pat: pat,
39+ }
40+
41+ reqUrl, err := url.Parse("https://api.individual.githubcopilot.com/chat/completions")
42+ if err != nil {
43+ t.Fatalf("failed to parse test URL: %v", err)
44+ }
45+
46+ req := &http.Request{
47+ Method: "POST",
48+ URL: reqUrl,
49+ Header: make(http.Header),
50+ }
51+ req = req.WithContext(context.Background())
52+
53+ _, err = transport.RoundTrip(req)
54+ if err != nil {
55+ t.Fatalf("RoundTrip failed: %v", err)
56+ }
57+
58+ if mockRT.lastReq == nil {
59+ t.Fatal("RoundTrip did not forward the request")
60+ }
61+
62+ // Verify both URL host and Host header are rewritten correctly to prevent 421 Misdirected Request
63+ wantHost := "api.enterprise.copilot.proxy"
64+ if mockRT.lastReq.URL.Host != wantHost {
65+ t.Errorf("expected URL.Host to be %q, got %q", wantHost, mockRT.lastReq.URL.Host)
66+ }
67+ if mockRT.lastReq.Host != wantHost {
68+ t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host)
69+ }
70+}
@@ -0,0 +1,70 @@
1+package provider
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/url"
7+ "testing"
8+ "time"
9+)
10+
11+type mockRoundTripper struct {
12+ lastReq *http.Request
13+}
14+
15+func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
16+ m.lastReq = req
17+ return &http.Response{
18+ StatusCode: http.StatusOK,
19+ Body: http.NoBody,
20+ }, nil
21+}
22+
23+func TestCopilotHostRewrite(t *testing.T) {
24+ // Seed the cache with a mock token that carries a non-default proxy endpoint
25+ // Ensure the expiresAt time is far in the future so that it bypasses the exchange check
26+ const pat = "ghp_mock_token_for_test"
27+ copilotCache.mu.Lock()
28+ copilotCache.tokens[pat] = copilotToken{
29+ value: "mock-jwt-with;proxy-ep=enterprise.copilot.proxy",
30+ expiresAt: time.Now().Add(1 * time.Hour),
31+ baseURL: "https://api.enterprise.copilot.proxy",
32+ }
33+ copilotCache.mu.Unlock()
34+
35+ mockRT := &mockRoundTripper{}
36+ transport := &copilotRefreshTransport{
37+ inner: mockRT,
38+ pat: pat,
39+ }
40+
41+ reqUrl, err := url.Parse("https://api.individual.githubcopilot.com/chat/completions")
42+ if err != nil {
43+ t.Fatalf("failed to parse test URL: %v", err)
44+ }
45+
46+ req := &http.Request{
47+ Method: "POST",
48+ URL: reqUrl,
49+ Header: make(http.Header),
50+ }
51+ req = req.WithContext(context.Background())
52+
53+ _, err = transport.RoundTrip(req)
54+ if err != nil {
55+ t.Fatalf("RoundTrip failed: %v", err)
56+ }
57+
58+ if mockRT.lastReq == nil {
59+ t.Fatal("RoundTrip did not forward the request")
60+ }
61+
62+ // Verify both URL host and Host header are rewritten correctly to prevent 421 Misdirected Request
63+ wantHost := "api.enterprise.copilot.proxy"
64+ if mockRT.lastReq.URL.Host != wantHost {
65+ t.Errorf("expected URL.Host to be %q, got %q", wantHost, mockRT.lastReq.URL.Host)
66+ }
67+ if mockRT.lastReq.Host != wantHost {
68+ t.Errorf("expected Host header (Request.Host) to be %q, got %q", wantHost, mockRT.lastReq.Host)
69+ }
70+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close