GraphQL-WS无法连接GQLgen服务?订阅连接报错求助
服务端基于gqlgen搭建GraphQL服务,前端使用Apollo Client结合graphql-ws实现订阅功能。该功能在GraphQL Playground中可正常运行,但前端客户端连接时出现错误:
WebSocket connection to 'ws://localhost:8080/' failed
容器日志显示:
unable to upgrade *http.response to websocket websocket: request origin not allowed by Upgrader.CheckOrigin: http: superfluous response.WriteHeader call...
已在WebSocket Upgrader中设置CheckOrigin返回true,但问题仍未解决。
服务端代码
if err := godotenv.Load(); err != nil { log.Fatal("Error loading environment variables file") } port := helpers.Env("PORT") if port == "" { port = defaultPort } router := chi.NewRouter() router.Use(cors.New(cors.Options{ AllowedOrigins: strings.Split(helpers.Env("ALLOWED_ORIGINS"), ","), AllowCredentials: true, Debug: helpers.Env("DEBUG") == "true", AllowedHeaders: []string{"*"}, }).Handler) srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolvers.Resolver{}})) srv.AddTransport(transport.POST{}) upgrader := &transport.Websocket{ Upgrader: websocket.Upgrader{ HandshakeTimeout: 1 * time.Minute, CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 1024, WriteBufferSize: 1024, }, KeepAlivePingInterval: 10 * time.Second, } srv.AddTransport(upgrader) srv.Use(extension.Introspection{}) if helpers.Env("MODE") == "PRODUCTION" { cache, err := apq.NewCache(24 * time.Hour) if err != nil { log.Fatalf("cannot create APQ redis cache: %v", err) } srv.Use(extension.AutomaticPersistedQuery{Cache: cache}) } go initWorkers() go runAsynqmon() router.Use(getHeadersMiddleware()) router.Handle("/", srv) if helpers.Env("MODE") == "DEVELOPMENT" { router.Handle("/playground", playground.Handler("GraphQL playground", "/")) log.Printf("connect to http://localhost:%s/playground for GraphQL playground", port) } log.Fatal(http.ListenAndServe(":"+port, router))
前端代码
import { setContext } from '@apollo/client/link/context' import { onError } from '@apollo/client/link/error' import { GraphQLWsLink } from '@apollo/client/link/subscriptions' import { getMainDefinition } from '@apollo/client/utilities' import { createUploadLink } from 'apollo-upload-client' import { createClient } from 'graphql-ws' import { logout } from '../helpers/logout' import { getTokenFromStorage } from '../helpers/userData' import { lang } from '../localization' const authLink = setContext((_, { headers }) => { const token = getTokenFromStorage() return { headers: { authorization: token ? `Bearer ${token}` : undefined, 'Accept-Language': lang, ...headers } } }) const httpLink = createUploadLink({ uri: process.env.REACT_APP_GRAPH_BFF || 'http://localhost:8080' }) const wsLink = new GraphQLWsLink( createClient({ url: 'ws://localhost:8080/' }) ) const splitLink = split( ({ query }) => { const definition = getMainDefinition(query) return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ) }, wsLink, httpLink ) const logoutLink = onError(({ response }) => { if ( response?.errors && response.errors.length > 0 && response.errors.some((errorItem) => errorItem.message.toLowerCase().includes('unauthenticated') ) ) { logout() } }) const chainList = [logoutLink, authLink, splitLink] const linkChain = from(chainList) const apolloClient = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), link: linkChain }) export default apolloClient
解决思路
1. 排查CORS中间件与WebSocket升级的冲突
当前使用的chi CORS中间件会在请求处理前提前设置响应头,而WebSocket升级需要完成握手流程,中间件提前调用WriteHeader会触发日志中的superfluous response.WriteHeader call错误:
- 检查
ALLOWED_ORIGINS环境变量是否包含前端实际的Origin(比如http://localhost:3000),确保CORS中间件不会提前拒绝WebSocket握手请求; - 尝试将CORS中间件的作用范围缩小,仅对HTTP POST请求生效,跳过WebSocket的GET握手请求;
- 调整中间件顺序,保证GraphQL服务的WebSocket处理逻辑优先于CORS中间件执行。
2. 修正WebSocket连接URL
前端中WebSocket的URL为ws://localhost:8080/,末尾的斜杠可能导致路径匹配问题,尝试修改为ws://localhost:8080,避免路径解析异常。
3. 检查自定义中间件getHeadersMiddleware()的影响
getHeadersMiddleware()可能在处理请求时提前调用了WriteHeader,干扰WebSocket升级流程。先注释该中间件进行测试,若问题解决,需修改中间件逻辑,让它在WebSocket请求时跳过响应头写入操作。
4. 修正前端WebSocket的认证参数传递
前端的authLink仅对HTTP请求生效,无法为WebSocket连接添加自定义头。需通过graphql-ws的connectionParams传递认证信息:
const wsLink = new GraphQLWsLink( createClient({ url: 'ws://localhost:8080', connectionParams: () => { const token = getTokenFromStorage() return { authorization: token ? `Bearer ${token}` : undefined, 'Accept-Language': lang } } }) )
这样能确保WebSocket握手时携带正确的认证参数,避免因头信息缺失或不兼容导致的连接失败。
5. 确认WebSocket传输的配置顺序
虽然当前服务端先添加POST传输再添加WebSocket传输的顺序没问题,但可尝试调整顺序,优先添加WebSocket传输,确保握手请求被优先处理。
内容的提问来源于stack exchange,提问作者nastaran jalali

