You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GraphQL-WS无法连接GQLgen服务?订阅连接报错求助

问题:GraphQL订阅功能前端连接失败排查

服务端基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 15:10:33