From c8c639e3d6818fa2af3176d0efdf13b78db30908 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 6 Nov 2026 11:01:02 +0100 Subject: [PATCH] fix(realtime): keep Reverb handshake under its size limit with large cookie headers Raise the Reverb max request size and store the remembered list view cookies unencrypted, so the WebSocket handshake no longer fails with 413 and live updates (like proxy restart logs) reach the browser. --- app/Http/Middleware/EncryptCookies.php | 10 ++++++++ config/reverb.php | 2 +- tests/Feature/DashboardCanvasTest.php | 8 +++--- tests/Feature/DashboardViewModeTest.php | 2 +- tests/Feature/ProjectCanvasTest.php | 16 ++++++------ tests/Feature/ServerCanvasTest.php | 30 +++++++++++----------- tests/Feature/SharedVariableCanvasTest.php | 8 +++--- tests/Feature/TagCanvasTest.php | 8 +++--- 8 files changed, 47 insertions(+), 37 deletions(-) diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index 867695bdcf..66b89002c1 100644 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -14,4 +14,14 @@ class EncryptCookies extends Middleware protected $except = [ // ]; + + /** + * The remembered list views (RemembersCanvasView) hold only an allow-listed view name. + * Encryption makes each of these per-user, per-page cookies about 330 bytes, and the + * browser sends all of them on the Reverb WebSocket handshake too. + */ + public function isDisabled($name) + { + return parent::isDisabled($name) || preg_match('/^coolify_\d+_[a-z_]+_view$/', $name) === 1; + } } diff --git a/config/reverb.php b/config/reverb.php index 6331511347..e3498baa63 100644 --- a/config/reverb.php +++ b/config/reverb.php @@ -36,7 +36,7 @@ return [ 'options' => [ 'tls' => [], ], - 'max_request_size' => 10_000, + 'max_request_size' => 64_000, 'scaling' => [ 'enabled' => false, 'channel' => 'reverb', diff --git a/tests/Feature/DashboardCanvasTest.php b/tests/Feature/DashboardCanvasTest.php index 9204b3e77a..d020ecffcc 100644 --- a/tests/Feature/DashboardCanvasTest.php +++ b/tests/Feature/DashboardCanvasTest.php @@ -116,17 +116,17 @@ test('the dashboard opens the canvas again when the user left it there', functio ->get(route('dashboard.canvas')) ->assertOk() ->assertSee('data-endpoint="/dashboard/canvas"', false) - ->assertCookie($cookie, 'canvas'); + ->assertPlainCookie($cookie, 'canvas'); dashboardCanvasSession($this, $this->admin, $this->team) - ->withCookie($cookie, 'canvas') + ->withUnencryptedCookie($cookie, 'canvas') ->get(route('dashboard')) ->assertRedirect(route('dashboard.canvas')); // The list link on the canvas opens the list and makes it the remembered view. dashboardCanvasSession($this, $this->admin, $this->team) - ->withCookie($cookie, 'canvas') + ->withUnencryptedCookie($cookie, 'canvas') ->get(route('dashboard', ['view' => 'list'])) ->assertOk() - ->assertCookie($cookie, 'list'); + ->assertPlainCookie($cookie, 'list'); }); diff --git a/tests/Feature/DashboardViewModeTest.php b/tests/Feature/DashboardViewModeTest.php index 008907103e..e810b9ed94 100644 --- a/tests/Feature/DashboardViewModeTest.php +++ b/tests/Feature/DashboardViewModeTest.php @@ -37,7 +37,7 @@ it('remembers the view of each dashboard section on its own', function () { expect(Cookie::queued("coolify_{$this->user->id}_dashboard_projects_view")?->getValue())->toBe('grid') ->and(Cookie::queued("coolify_{$this->user->id}_dashboard_servers_view"))->toBeNull(); - $this->withCookie("coolify_{$this->user->id}_dashboard_projects_view", 'grid') + $this->withUnencryptedCookie("coolify_{$this->user->id}_dashboard_projects_view", 'grid') ->get(route('dashboard')) ->assertOk() ->assertSee("viewMode: 'grid'", false) diff --git a/tests/Feature/ProjectCanvasTest.php b/tests/Feature/ProjectCanvasTest.php index db02a05736..d2efe040dc 100644 --- a/tests/Feature/ProjectCanvasTest.php +++ b/tests/Feature/ProjectCanvasTest.php @@ -134,19 +134,19 @@ test('the project page reopens the canvas when the user left it on the canvas vi ->get(route('project.canvas', $parameters)) ->assertOk() ->assertSee('data-endpoint="/projects/'.$this->project->uuid.'/canvas"', false) - ->assertCookie("coolify_{$this->admin->id}_project_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_project_view", 'canvas'); projectCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_project_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_project_view", 'canvas') ->get(route('project.show', $parameters)) ->assertRedirect(route('project.canvas', $parameters)); // The table / grid links on the canvas pick the list view again. projectCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_project_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_project_view", 'canvas') ->get(route('project.show', [...$parameters, 'view' => 'grid'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_project_view", 'grid'); + ->assertPlainCookie("coolify_{$this->admin->id}_project_view", 'grid'); }); test('the projects canvas shows every project of the team as a zone, grouped by environment', function () { @@ -230,18 +230,18 @@ test('the projects page reopens the canvas when the user left it on the canvas v projectCanvasSession($this, $this->admin, $this->team) ->get(route('project.canvas.index')) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_projects_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_projects_view", 'canvas'); projectCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_projects_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_projects_view", 'canvas') ->get(route('project.index')) ->assertRedirect(route('project.canvas.index')); projectCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_projects_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_projects_view", 'canvas') ->get(route('project.index', ['view' => 'table'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_projects_view", 'table'); + ->assertPlainCookie("coolify_{$this->admin->id}_projects_view", 'table'); }); test('an admin creates an environment from the project canvas', function () { diff --git a/tests/Feature/ServerCanvasTest.php b/tests/Feature/ServerCanvasTest.php index a3ec0b4bd0..14c1dcbd5c 100644 --- a/tests/Feature/ServerCanvasTest.php +++ b/tests/Feature/ServerCanvasTest.php @@ -221,22 +221,22 @@ test('the servers page reopens the canvas when the user left it on the canvas vi serverCanvasSession($this, $this->admin, $this->team) ->get(route('server.canvas.index')) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_servers_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_servers_view", 'canvas'); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_servers_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas') ->get(route('server.index')) ->assertRedirect(route('server.canvas.index')); // The table / grid links on the canvas pick the list view again. serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_servers_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas') ->get(route('server.index', ['view' => 'grid'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_servers_view", 'grid'); + ->assertPlainCookie("coolify_{$this->admin->id}_servers_view", 'grid'); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_servers_view", 'grid') + ->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'grid') ->get(route('server.index')) ->assertOk(); }); @@ -247,18 +247,18 @@ test('the environment page reopens the canvas when the user left it on the canva serverCanvasSession($this, $this->admin, $this->team) ->get(route('project.environment.canvas', $parameters)) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_environment_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_environment_view", 'canvas'); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_environment_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_environment_view", 'canvas') ->get(route('project.resource.index', $parameters)) ->assertRedirect(route('project.environment.canvas', $parameters)); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_environment_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_environment_view", 'canvas') ->get(route('project.resource.index', [...$parameters, 'view' => 'table'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_environment_view", 'table'); + ->assertPlainCookie("coolify_{$this->admin->id}_environment_view", 'table'); }); test('the server resources page reopens the one server canvas when the user left it on the canvas view', function () { @@ -267,18 +267,18 @@ test('the server resources page reopens the one server canvas when the user left serverCanvasSession($this, $this->admin, $this->team) ->get(route('server.canvas', $parameters)) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas'); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas') ->get(route('server.resources', $parameters)) ->assertRedirect(route('server.canvas', $parameters)); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_server_resources_view", 'canvas') ->get(route('server.resources', [...$parameters, 'view' => 'list'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_server_resources_view", 'list'); + ->assertPlainCookie("coolify_{$this->admin->id}_server_resources_view", 'list'); }); test('the canvas API works with the browser session when the instance API is disabled', function () { @@ -295,7 +295,7 @@ test('the remembered view belongs to the user, not to the browser', function () $this->team->members()->attach($other->id, ['role' => 'admin']); serverCanvasSession($this, $other, $this->team) - ->withCookie("coolify_{$this->admin->id}_servers_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_servers_view", 'canvas') ->get(route('server.index')) ->assertOk(); }); @@ -315,7 +315,7 @@ test('the list page remembers the table or grid choice and opens it again', func expect(Cookie::getQueuedCookies())->toBeEmpty(); serverCanvasSession($this, $this->admin, $this->team) - ->withCookie($cookie, 'grid') + ->withUnencryptedCookie($cookie, 'grid') ->get(route('server.index')) ->assertOk() ->assertSee("viewMode: 'grid'", false); diff --git a/tests/Feature/SharedVariableCanvasTest.php b/tests/Feature/SharedVariableCanvasTest.php index 92b3db0f6c..89b7d5d487 100644 --- a/tests/Feature/SharedVariableCanvasTest.php +++ b/tests/Feature/SharedVariableCanvasTest.php @@ -297,18 +297,18 @@ test('each shared variables page reopens its canvas when the user left it on the sharedVariableCanvasAs($this, $this->admin, $this->team) ->get(route("shared-variables.{$scope}.canvas", $parameters)) ->assertOk() - ->assertCookie($cookie, 'canvas'); + ->assertPlainCookie($cookie, 'canvas'); sharedVariableCanvasAs($this, $this->admin, $this->team) - ->withCookie($cookie, 'canvas') + ->withUnencryptedCookie($cookie, 'canvas') ->get(route($listRoute, $parameters)) ->assertRedirect(route("shared-variables.{$scope}.canvas", $parameters)); sharedVariableCanvasAs($this, $this->admin, $this->team) - ->withCookie($cookie, 'canvas') + ->withUnencryptedCookie($cookie, 'canvas') ->get(route($listRoute, [...$parameters, 'view' => 'list'])) ->assertOk() - ->assertCookie($cookie, 'list'); + ->assertPlainCookie($cookie, 'list'); })->with(['team', 'project', 'environment', 'server']); test('an admin adds a shared variable from the canvas modal and the canvas reloads', function () { diff --git a/tests/Feature/TagCanvasTest.php b/tests/Feature/TagCanvasTest.php index e54af1fcea..88d9b196d5 100644 --- a/tests/Feature/TagCanvasTest.php +++ b/tests/Feature/TagCanvasTest.php @@ -211,16 +211,16 @@ test('the tags page reopens the canvas when the user left it on the canvas view' tagCanvasAs($this, $this->admin, $this->team) ->get(route('tags.canvas')) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_tags_view", 'canvas'); + ->assertPlainCookie("coolify_{$this->admin->id}_tags_view", 'canvas'); tagCanvasAs($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_tags_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_tags_view", 'canvas') ->get(route('tags.show')) ->assertRedirect(route('tags.canvas')); tagCanvasAs($this, $this->admin, $this->team) - ->withCookie("coolify_{$this->admin->id}_tags_view", 'canvas') + ->withUnencryptedCookie("coolify_{$this->admin->id}_tags_view", 'canvas') ->get(route('tags.show', ['view' => 'grid'])) ->assertOk() - ->assertCookie("coolify_{$this->admin->id}_tags_view", 'grid'); + ->assertPlainCookie("coolify_{$this->admin->id}_tags_view", 'grid'); });