Skip to content

Commit df322f4

Browse files
aspiersClaude Code
andcommitted
fix: use relative paths in GraphiQL to support multiple domains
Without this patch, GraphiQL endpoints were configured with absolute URLs constructed from ExternalBaseURL. This caused GraphiQL to fail when the service was accessed through a different domain (e.g., via proxy, load balancer, or alternate hostname) because it would always try to connect to the configured base URL rather than the domain the user was actually using. This is a problem because it prevents GraphiQL from working in multi-domain deployments and makes local development harder when accessing the service through different hostnames. This patch solves the problem by changing GraphiQLConfig to accept relative paths (EndpointPath and SubscriptionPath) instead of absolute URLs. The GraphiQL HTML template now uses JavaScript to dynamically construct the full URLs from window.location at runtime, ensuring the page always connects to the correct domain. The WebSocket protocol is also derived dynamically (ws: for http:, wss: for https:). Changes: - Replace Endpoint/SubscriptionEndpoint with EndpointPath/SubscriptionPath - Use window.location.origin to build full GraphQL URL at runtime - Derive WebSocket protocol from page protocol dynamically - Update tests to verify path-based configuration Co-authored-by: Claude Code <claude-code@noreply.anthropic.com>
1 parent 9de3ef9 commit df322f4

3 files changed

Lines changed: 31 additions & 34 deletions

File tree

cmd/hypergoat/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,9 @@ func setupAdmin(r *chi.Mux, cfg *config.Config, svc *services) *admin.Handler {
431431

432432
// GraphiQL playgrounds
433433
r.Get("/graphiql", server.HandleGraphiQL(server.GraphiQLConfig{
434-
Endpoint: cfg.ExternalBaseURL + "/graphql",
435-
SubscriptionEndpoint: strings.Replace(cfg.ExternalBaseURL, "http", "ws", 1) + "/graphql/ws",
436-
Title: "Hypergoat GraphQL",
434+
EndpointPath: "/graphql",
435+
SubscriptionPath: "/graphql/ws",
436+
Title: "Hypergoat GraphQL",
437437
DefaultQuery: `# Hypergoat GraphQL API
438438
#
439439
# Explore the AT Protocol data indexed by this AppView.
@@ -451,8 +451,8 @@ func setupAdmin(r *chi.Mux, cfg *config.Config, svc *services) *admin.Handler {
451451
}))
452452

453453
r.Get("/graphiql/admin", server.HandleGraphiQL(server.GraphiQLConfig{
454-
Endpoint: cfg.ExternalBaseURL + "/admin/graphql",
455-
Title: "Hypergoat Admin",
454+
EndpointPath: "/admin/graphql",
455+
Title: "Hypergoat Admin",
456456
DefaultQuery: `# Hypergoat Admin API
457457
#
458458
# Administrative operations for managing the AppView.

internal/server/graphiql.go

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99

1010
// GraphiQLConfig contains configuration for the GraphiQL handler.
1111
type GraphiQLConfig struct {
12-
// Endpoint is the GraphQL endpoint URL.
13-
Endpoint string
14-
// SubscriptionEndpoint is the WebSocket endpoint for subscriptions (optional).
15-
SubscriptionEndpoint string
12+
// EndpointPath is the path to the GraphQL endpoint (e.g. "/graphql").
13+
// The full URL is derived at runtime from the browser's window.location.
14+
EndpointPath string
15+
// SubscriptionPath is the path for WebSocket subscriptions (optional, e.g. "/graphql/ws").
16+
SubscriptionPath string
1617
// Title is the page title.
1718
Title string
1819
// DefaultQuery is the initial query to display.
@@ -71,22 +72,14 @@ func generateGraphiQLHTML(cfg GraphiQLConfig) string {
7172
`
7273
}
7374

74-
// Build fetcher config
75-
fetcherConfig := `{
76-
url: '` + cfg.Endpoint + `',
77-
headers: {
78-
'Content-Type': 'application/json',
79-
},
80-
}`
81-
82-
if cfg.SubscriptionEndpoint != "" {
83-
fetcherConfig = `{
84-
url: '` + cfg.Endpoint + `',
85-
headers: {
86-
'Content-Type': 'application/json',
87-
},
88-
subscriptionUrl: '` + cfg.SubscriptionEndpoint + `',
89-
}`
75+
// Build subscription URL JavaScript snippet.
76+
// Uses window.location to derive the correct WebSocket URL at runtime,
77+
// so the page works regardless of which domain it's accessed through.
78+
subscriptionJS := ""
79+
if cfg.SubscriptionPath != "" {
80+
subscriptionJS = `
81+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
82+
fetcherOpts.subscriptionUrl = wsProto + '//' + location.host + '` + cfg.SubscriptionPath + `';`
9083
}
9184

9285
return `<!DOCTYPE html>
@@ -114,8 +107,12 @@ func generateGraphiQLHTML(cfg GraphiQLConfig) string {
114107
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
115108
<script crossorigin src="https://unpkg.com/graphiql@3/graphiql.min.js"></script>
116109
<script>
110+
const fetcherOpts = {
111+
url: location.origin + '` + cfg.EndpointPath + `',
112+
headers: { 'Content-Type': 'application/json' },
113+
};` + subscriptionJS + `
117114
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
118-
const fetcher = GraphiQL.createFetcher(` + fetcherConfig + `);
115+
const fetcher = GraphiQL.createFetcher(fetcherOpts);
119116
root.render(
120117
React.createElement(GraphiQL, {
121118
fetcher: fetcher,

internal/server/handlers_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ func TestHandleClientMetadata(t *testing.T) {
314314

315315
func TestHandleGraphiQL(t *testing.T) {
316316
baseCfg := GraphiQLConfig{
317-
Endpoint: "/graphql",
318-
Title: "Hypergoat GraphiQL",
317+
EndpointPath: "/graphql",
318+
Title: "Hypergoat GraphiQL",
319319
}
320320

321321
t.Run("GET returns 200 with text/html content type", func(t *testing.T) {
@@ -363,9 +363,9 @@ func TestHandleGraphiQL(t *testing.T) {
363363

364364
t.Run("subscription endpoint included when configured", func(t *testing.T) {
365365
cfg := GraphiQLConfig{
366-
Endpoint: "/graphql",
367-
SubscriptionEndpoint: "ws://localhost:8080/graphql/ws",
368-
Title: "Test",
366+
EndpointPath: "/graphql",
367+
SubscriptionPath: "/graphql/ws",
368+
Title: "Test",
369369
}
370370
handler := HandleGraphiQL(cfg)
371371
req := httptest.NewRequest(http.MethodGet, "/graphiql", nil)
@@ -374,8 +374,8 @@ func TestHandleGraphiQL(t *testing.T) {
374374
handler.ServeHTTP(rec, req)
375375

376376
body := rec.Body.String()
377-
if !strings.Contains(body, "ws://localhost:8080/graphql/ws") {
378-
t.Error("response body does not contain subscription endpoint")
377+
if !strings.Contains(body, "/graphql/ws") {
378+
t.Error("response body does not contain subscription path")
379379
}
380380
if !strings.Contains(body, "subscriptionUrl") {
381381
t.Error("response body does not contain subscriptionUrl config key")
@@ -409,7 +409,7 @@ func TestHandleGraphiQL(t *testing.T) {
409409

410410
t.Run("default title used when not configured", func(t *testing.T) {
411411
cfg := GraphiQLConfig{
412-
Endpoint: "/graphql",
412+
EndpointPath: "/graphql",
413413
}
414414
handler := HandleGraphiQL(cfg)
415415
req := httptest.NewRequest(http.MethodGet, "/graphiql", nil)

0 commit comments

Comments
 (0)