From df322f40df54f249b72e90afbaa5d93b10cb10ec Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Thu, 12 Feb 2026 23:35:34 +1300 Subject: [PATCH 1/2] 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 --- cmd/hypergoat/main.go | 10 ++++---- internal/server/graphiql.go | 39 +++++++++++++++----------------- internal/server/handlers_test.go | 16 ++++++------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/cmd/hypergoat/main.go b/cmd/hypergoat/main.go index 63c0151..bde1695 100644 --- a/cmd/hypergoat/main.go +++ b/cmd/hypergoat/main.go @@ -431,9 +431,9 @@ func setupAdmin(r *chi.Mux, cfg *config.Config, svc *services) *admin.Handler { // GraphiQL playgrounds r.Get("/graphiql", server.HandleGraphiQL(server.GraphiQLConfig{ - Endpoint: cfg.ExternalBaseURL + "/graphql", - SubscriptionEndpoint: strings.Replace(cfg.ExternalBaseURL, "http", "ws", 1) + "/graphql/ws", - Title: "Hypergoat GraphQL", + EndpointPath: "/graphql", + SubscriptionPath: "/graphql/ws", + Title: "Hypergoat GraphQL", DefaultQuery: `# Hypergoat GraphQL API # # 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 { })) r.Get("/graphiql/admin", server.HandleGraphiQL(server.GraphiQLConfig{ - Endpoint: cfg.ExternalBaseURL + "/admin/graphql", - Title: "Hypergoat Admin", + EndpointPath: "/admin/graphql", + Title: "Hypergoat Admin", DefaultQuery: `# Hypergoat Admin API # # Administrative operations for managing the AppView. diff --git a/internal/server/graphiql.go b/internal/server/graphiql.go index 0dba001..9f2f916 100644 --- a/internal/server/graphiql.go +++ b/internal/server/graphiql.go @@ -9,10 +9,11 @@ import ( // GraphiQLConfig contains configuration for the GraphiQL handler. type GraphiQLConfig struct { - // Endpoint is the GraphQL endpoint URL. - Endpoint string - // SubscriptionEndpoint is the WebSocket endpoint for subscriptions (optional). - SubscriptionEndpoint string + // EndpointPath is the path to the GraphQL endpoint (e.g. "/graphql"). + // The full URL is derived at runtime from the browser's window.location. + EndpointPath string + // SubscriptionPath is the path for WebSocket subscriptions (optional, e.g. "/graphql/ws"). + SubscriptionPath string // Title is the page title. Title string // DefaultQuery is the initial query to display. @@ -71,22 +72,14 @@ func generateGraphiQLHTML(cfg GraphiQLConfig) string { ` } - // Build fetcher config - fetcherConfig := `{ - url: '` + cfg.Endpoint + `', - headers: { - 'Content-Type': 'application/json', - }, - }` - - if cfg.SubscriptionEndpoint != "" { - fetcherConfig = `{ - url: '` + cfg.Endpoint + `', - headers: { - 'Content-Type': 'application/json', - }, - subscriptionUrl: '` + cfg.SubscriptionEndpoint + `', - }` + // Build subscription URL JavaScript snippet. + // Uses window.location to derive the correct WebSocket URL at runtime, + // so the page works regardless of which domain it's accessed through. + subscriptionJS := "" + if cfg.SubscriptionPath != "" { + subscriptionJS = ` + const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + fetcherOpts.subscriptionUrl = wsProto + '//' + location.host + '` + cfg.SubscriptionPath + `';` } return ` @@ -114,8 +107,12 @@ func generateGraphiQLHTML(cfg GraphiQLConfig) string { -