{"record":{"id":"6b5a73ac4ff7af2b","repo":"phacility/phabricator","slug":"got-http-200-but-expected-http-501-websocket-upg","errorCode":null,"errorMessage":"Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!","messagePattern":"Got HTTP 200, but expected HTTP 501 \\(WebSocket Upgrade\\)!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/notification/client/PhabricatorNotificationServerRef.php","lineNumber":211,"sourceCode":"      throw new Exception(\n        pht('Unable to test client on an admin server!'));\n    }\n\n    $server_uri = $this->getURI();\n\n    try {\n      id(new HTTPSFuture($server_uri))\n        ->setTimeout(2)\n        ->resolvex();\n    } catch (HTTPFutureHTTPResponseStatus $ex) {\n      // This is what we expect when things are working correctly.\n      if ($ex->getStatusCode() == 501) {\n        return true;\n      }\n      throw $ex;\n    }\n\n    throw new Exception(\n      pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!'));\n  }\n\n  public function loadServerStatus() {\n    if (!$this->isAdminServer()) {\n      throw new Exception(\n        pht(\n          'Unable to load server status: this is not an admin server!'));\n    }\n\n    $server_uri = $this->getURI('/status/');\n\n    list($body) = $this->newFuture($server_uri)\n      ->resolvex();\n\n    return phutil_json_decode($body);\n  }\n","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/notification/client/PhabricatorNotificationServerRef.php#L193-L229","documentation":"PhabricatorNotificationServerRef::testClient() probes the notification server by issuing a plain HTTPSFuture GET to the client URI with a 2 second timeout. The Aphlict server correctly answers non-websocket HTTP with 501 'Not Implemented', so the catch block treats HTTPFutureHTTPResponseStatus with status 501 as success. If instead the request resolves without an exception (any 2xx, the message says 200), something else — usually a reverse proxy, load balancer, or the wrong port — is answering, and the method throws this generic Exception.","triggerScenarios":"Calling $server_ref->testClient() (used by bin/notifications workflows and diagnostics) where getURI() points at a client port fronted by nginx/Apache/HAProxy that returns 200 for GET, or the URI was configured with an admin-port/web port instead of the Aphlict client port. resolvex() succeeds, execution falls through the try block, and the throw at line 211 fires.","commonSituations":"A proxy or health-check layer in front of every port; notification.servers config listing wrong host/port so the probe hits the web phabricator instance (which answers 200); TLS termination rewriting behavior; a firewall/redirect answering with 200; testing after adding an LB without excluding the Aphlict client port.","solutions":["Fix notification.servers so the client server entry points directly at the Aphlict client port (default 22280/22261 with TLS), not through the proxy or at the web port.","If a proxy must sit in front, make it pass WebSocket upgrades on that port rather than serving 200 itself.","Re-run the probe after the change: the client port must answer plain GET with 501 for testClient() to return true.","Verify with curl: curl -i http://host:client-port/ should yield HTTP/1.1 501; a 200 identifies whatever is intercepting."],"exampleFix":"// before (notification.servers):\n[{\"type\":\"client\",\"host\":\"phabricator.example\",\"port\":443,...}]\n// port 443 is the web server -> GET returns 200 -> err: \"Got HTTP 200, but expected HTTP 501\"\n\n// after:\n[{\"type\":\"client\",\"host\":\"phabricator.example\",\"port\":22261,\"protocol\":\"https\",...}]\n// Aphlict answers GET with 501 -> testClient() === true","handlingStrategy":"validation","validationCode":"// Before calling testClient(), verify plain GET yields 501 from the exact client URI:\nlist($status) = id(new HTTPSFuture($ref->getURI()))\n  ->setTimeout(2)\n  ->resolve();\nif (!($status instanceof HTTPFutureResponseStatus && $status->getStatusCode() === 501)) {\n  // a proxy/web server is answering on the client port: fix notification.servers before testing\n}","typeGuard":null,"tryCatchPattern":"try {\n  $ok = $ref->testClient();\n} catch (Exception $ex) {\n  if (preg_match('/expected HTTP 501/', $ex->getMessage())) {\n    // wrong endpoint answering (proxy/LB/web port): correct the client entry in\n    // notification.servers, exempt the Aphlict port from the proxy, then retry once\n  }\n  throw $ex;\n}","preventionTips":["Point the client entry of notification.servers straight at the Aphlict client port; never route it through a web proxy or the web vhost.","Health-check with curl: plain GET on the client port must answer 501; a 200 identifies an interceptor.","Re-run testClient() after every load-balancer or reverse-proxy change."],"tags":["notifications","aphlict","websocket","http-status","config","proxy"],"backgroundTag":"wrong-service-port","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}