如何修复Firefox无法建立wss://testdrive.co/subscription连接的问题?
Firefox下WebSocket连接频繁失败的排查与解决
问题现象
服务在Chrome、Opera等浏览器运行正常,但Firefox中频繁出现「Firefox无法连接到服务器wss://testdrive.co/subscription」的错误,5次尝试中有3次触发该问题。
现有配置
Node.js(app.js)配置
/** * Importing Packages */ require("dotenv").config({ path: `./env/.env-${process.env.NODE_ENV}` }); const express = require("express"); const logger = require("morgan"); const moment = require("moment"); const cors = require("cors"); const path = require("path"); const bodyParser = require('body-parser'); const app = express(); const fs = require("fs"); const { ApolloServer, gql } = require("apollo-server-express"); const { mergeGraphQLTypes, mergeResolvers } = require("@graphql-tools/merge"); const { makeExecutableSchema } = require("@graphql-tools/schema"); const { applyMiddleware } = require("graphql-middleware"); const { createServer } = require("http"); // newly added const { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault } = require("apollo-server-core"); const { WebSocketServer } = require("ws"); const { useServer } = require("graphql-ws/lib/use/ws"); const graphqlUploadExpress = require("graphql-upload/graphqlUploadExpress.js"); const GraphQLUpload = require("graphql-upload/GraphQLUpload.js"); require("./database/db"); require("./helper/function"); const Stripe = require("./stripe/stripeRoute"); const Cron = require("./scheduleCron/scheduleCronRoute"); const generateFolder = Helper("generate-folder"); /* Get the Queries & Mutation type and merge them togather for the schema */ const Query = mergeGraphQLTypes(require("./queries")(gql)); /* Get the resolvers array and merge them togather for the schema */ const Resolvers = mergeResolvers(require("./resolvers")); Resolvers.Upload = GraphQLUpload; /* Get all the middlewares. Note:- This we can return as array of middleware object The object pattern are mentioned in the middleware folder index.js file */ const Middlewares = require("./middlewares"); /* For the use of middleware we must create an executable schema and pass the query and resolvers we acquired before. */ const schema = makeExecutableSchema({ typeDefs: Query, resolvers: Resolvers }); /* Apply middlewares to the schema. */ const schemaWithMiddleware = applyMiddleware(schema, Middlewares.verifyUser); /* Create the main Apollo Server from the schema */ // Added for Websocket Subscriptions Starts // const httpServer = createServer(app); // Set up WebSocket server. const wsServer = new WebSocketServer({ //port: 8443, server: httpServer, path: "/", }); const serverCleanup = useServer({schema}, wsServer); // Added for Websocket Subscriptions Ends // const server = new ApolloServer({ schema: schemaWithMiddleware, context: ({ req, res }) => { return { req, res }; }, formatError: (err) => { // logger.info(err); return err; }, uploads: false, plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({httpServer}), { async serverWillStart() { return { async renderLandingPage() { const html = `<html> <head> <title>Welcome To TestDrive</title> </head> <body> <img src="https://testdrive.co/images/api-screen.jpg" style="display: block; margin-left: auto; margin-right: auto; width:auto;"> </body> </html>`; return { html }; }, async drainServer() { await serverCleanup.dispose(); }, } } } ] }); /* Setting up port */ const port = process.env.PORT || 8000; /* Generating logs */ var accessLogStream = fs.createWriteStream( path.join( __dirname, `${generateFolder.generateLogFolder()}/access-${moment().format( "YYYY-MM-DD" )}.log` ), { flags: "a" } ); // setting up the logger app.use(logger("combined", { stream: accessLogStream })); app.use(function (err, req, res, next) { logger.error( `${req.method} - ${err.message} - ${req.originalUrl} - ${req.ip}` ); next(err); }); /* Setting up stripe payment */ app.use( express.json({ // We need the raw body to verify webhook signatures. // Let's compute it only when hitting the Stripe webhook endpoint. verify: function (req, res, buf) { if (req.originalUrl.startsWith("/webhook")) { req.rawBody = buf.toString(); } }, }) ); /* Setting up graphql upload */ app.use( graphqlUploadExpress({ maxFileSize: 30000000, maxFiles: 20, }) ); /* Cors Setup */ var corsOptions = { origin: process.env.CORS_ALLOW_URL, allowedHeaders: [ "Content-Type", "Authorization", "Accept", "x-www-form-urlencoded", "x-access-token", ], credentials: true, }; app.use(cors(corsOptions)); app.use(express.static("public")); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(Stripe); app.use(Cron); /* Initiating the Apollo server */ server .start() .then((r) => { /** * The following line is require as we are using the apollo-server-express */ server.applyMiddleware({ app, path: "/" }); // Now that our HTTP server is fully set up, actually listen. httpServer.listen(port, () => { console.log(`🚀 Query endpoint ready at http://localhost:${port}${server.graphqlPath}`); console.log(`🚀 Subscription endpoint ready at ws://localhost:${port}${server.graphqlPath}`); }); }) .catch((err) => { console.log("Could not start the apollo server", err); });
Nginx配置
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream sockettest.testdrive.co { server 100.242.100.242:5555; # host and port of local running server instance } server { ssl on; listen 443 ssl http2; server_name apistg.testdrive.co; ssl_certificate /etc/testdrives/testcert.pem; ssl_certificate_key /etc/testdrives/testkey.pem; root /var/www/apistg.testdrive.co/html; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; location / { proxy_pass http://100.242.100.242:5555/; proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } location /subscription { # websocket endpoint proxy_pass http://sockettest.testdrive.co/; # upstream url and host proxy_redirect default; proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 10m; client_body_buffer_size 128k; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; } location /graphql { proxy_pass http://100.242.100.242:5555/graphql; proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } # Secure Headers add_header X-Frame-Options "SAMEORIGIN" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "origin-when-cross-origin" always; add_header "X-XSS-Protection" "1; mode=block"; }
修复方案
1. 修正WebSocket路径匹配
Node.js中WebSocketServer的路径设为"/",但Nginx的location是/subscription,路径不匹配会导致Firefox握手失败概率升高:
- 修改Node.js中的WebSocket路径:
const wsServer = new WebSocketServer({ server: httpServer, path: "/subscription", // 与Nginx的location对应 });
- 调整Apollo Server的路径到
/graphql,避免根路径请求混淆:
server.applyMiddleware({ app, path: "/graphql" });
2. 优化Nginx WebSocket配置
Firefox对WebSocket代理的超时和缓存设置更敏感,调整Nginx的/subscription配置:
location /subscription { proxy_pass http://sockettest.testdrive.co; # 去掉末尾的/,避免路径重写 proxy_redirect default; proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 10m; client_body_buffer_size 128k; proxy_connect_timeout 90; proxy_send_timeout 300s; # 延长发送超时 proxy_read_timeout 300s; # 延长读取超时 proxy_cache off; # 禁用缓存 }
3. 强化CORS配置
Firefox对跨域头的检查更严格,补充WebSocket相关的CORS配置:
var corsOptions = { origin: process.env.CORS_ALLOW_URL, allowedHeaders: [ "Content-Type", "Authorization", "Accept", "x-www-form-urlencoded", "x-access-token", "Upgrade", // 添加WebSocket升级头 "Connection" ], credentials: true, exposedHeaders: ["Upgrade"] // 暴露升级头给客户端 };
4. 排查SSL证书有效性
确保证书由受信任的CA颁发,检查证书的域名匹配度、有效期、证书链完整性。Firefox对自签名或非信任CA的证书容忍度远低于Chrome,必须使用合规的SSL证书。
5. 避免中间件干扰WebSocket握手
全局应用bodyParser可能会解析WebSocket的握手请求,导致失败,改为仅对需要的路径应用:
// 仅对/graphql和/webhook应用bodyParser app.use("/graphql", bodyParser.json()); app.use("/webhook", bodyParser.urlencoded({ extended: false }));
内容的提问来源于stack exchange,提问作者Saswat
相关产品推荐
相关产品推荐

