{"record":{"id":"c0eb98110be23348","repo":"nats-io/nats-server","slug":"websocket-invalid-header-q-not-allowed","errorCode":null,"errorMessage":"websocket: invalid header %q not allowed","messagePattern":"websocket: invalid header %q not allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/websocket.go","lineNumber":1187,"sourceCode":"\tif wo.JWTCookie != _EMPTY_ {\n\t\tif len(o.TrustedOperators) == 0 && len(o.TrustedKeys) == 0 {\n\t\t\treturn fmt.Errorf(\"trusted operators or trusted keys configuration is required for JWT authentication via cookie %q\", wo.JWTCookie)\n\t\t}\n\t}\n\tif err := validatePinnedCerts(wo.TLSPinnedCerts); err != nil {\n\t\treturn fmt.Errorf(\"websocket: %v\", err)\n\t}\n\n\t// Check for invalid headers here.\n\tfor key := range wo.Headers {\n\t\tk := strings.ToLower(key)\n\t\tswitch k {\n\t\tcase \"host\",\n\t\t\t\"content-length\",\n\t\t\t\"connection\",\n\t\t\t\"upgrade\",\n\t\t\t\"nats-no-masking\":\n\t\t\treturn fmt.Errorf(\"websocket: invalid header %q not allowed\", key)\n\t\t}\n\n\t\tif strings.HasPrefix(k, \"sec-websocket-\") {\n\t\t\treturn fmt.Errorf(\"websocket: invalid header %q, \\\"Sec-WebSocket-\\\" prefix not allowed\", key)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Creates or updates the existing map\nfunc (s *Server) wsSetOriginOptions(o *WebsocketOpts) {\n\tws := &s.websocket\n\tws.mu.Lock()\n\tdefer ws.mu.Unlock()\n\t// Copy over the option's same origin boolean\n\tws.sameOrigin = o.SameOrigin\n\t// Reset the map. Will help for config reload if/when we support it.","sourceCodeStart":1169,"sourceCodeEnd":1205,"githubUrl":"https://github.com/nats-io/nats-server/blob/3a66a489d262bf89b71a71c955c94920394532f3/server/websocket.go#L1169-L1205","documentation":"The websocket options block defines custom headers to add to handshake responses, but one of them is in the forbidden set (host, content-length, connection, upgrade, nats-no-masking). These headers are controlled by the HTTP/WebSocket protocol layer and cannot be overridden, so the validator fails startup.","triggerScenarios":"websocket { headers: { \"Connection\": \"keep-alive\" } } or similarly setting Host/Content-Length/Upgrade/Nats-No-Masking in the websocket headers map.","commonSituations":"Operators trying to add CORS or proxy-related headers and accidentally including hop-by-hop headers like Connection; copying header sets from reverse-proxy configs.","solutions":["Remove the offending header key from the websocket.headers map","Set Connection/Upgrade/Host behavior at the reverse proxy instead of in nats-server config","Use only application-safe custom headers (e.g. X-Forwarded-For style custom values are allowed)"],"exampleFix":"// before\nwebsocket { headers: { \"Connection\": \"keep-alive\" } }\n// after\nwebsocket { headers: { \"X-Custom-Header\": \"value\" } }","handlingStrategy":"validation","validationCode":"for key := range opts.Websocket.Headers {\n  switch strings.ToLower(key) {\n  case \"host\", \"content-length\", \"connection\", \"upgrade\", \"nats-no-masking\":\n    return fmt.Errorf(\"reserved header %q not configurable\", key)\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only add application-specific headers in websocket.headers","Handle hop-by-hop headers (Connection, Upgrade) at the proxy layer"],"tags":["websocket","headers","config-validation"],"backgroundTag":"reserved-http-header","analyzedSha":"3a66a489d262bf89b71a71c955c94920394532f3","analyzedAt":"2026-09-02T04:41:54.247Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}