From 6b7832acd058f069880a7a282628adbd58770e7d Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 30 Sep 2026 01:36:26 +0200 Subject: [PATCH] Fix stale miniserv configuration reads This PR fixes WebSocket routes being lost when miniserv configuration changes multiple times within one second. Read `miniserv.conf` directly so locked updates use the current configuration. The previous cache could reuse stale data because it compared modification times in whole seconds. Add a regression test covering route additions and removals with an unchanged modification time. --- t/web-lib-funcs-miniserv-config.t | 49 +++++++++++++++++++++++++++++++ web-lib-funcs.pl | 4 ++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 t/web-lib-funcs-miniserv-config.t diff --git a/t/web-lib-funcs-miniserv-config.t b/t/web-lib-funcs-miniserv-config.t new file mode 100644 index 000000000..5c9765334 --- /dev/null +++ b/t/web-lib-funcs-miniserv-config.t @@ -0,0 +1,49 @@ +#!/usr/bin/perl +# Concurrent WebSocket updates must not reuse a same-second config snapshot. +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); +use File::Basename qw(dirname); +use File::Spec; + +require File::Spec->rel2abs(dirname(__FILE__).'/../web-lib-funcs.pl'); +my $directory = tempdir(CLEANUP => 1); +my $file = "$directory/miniserv.conf"; +{ + no warnings qw(redefine once); + *main::get_miniserv_config_file = sub { return $file; }; +} + +# replace_config(contents) simulates another process writing within one second. +sub replace_config +{ +my ($contents) = @_; +open(my $fh, '>', $file) or die $!; +print {$fh} $contents; +close($fh) or die $!; +utime(1700000000, 1700000000, $file) or die $!; +} + +replace_config("port=10000\nwebsockets_/test/ws-555=first\n"); +my %first; +ok(main::get_miniserv_config(\%first), 'read initial configuration'); +is($first{'websockets_/test/ws-555'}, 'first', 'first route is present'); + +# Allocation must retain a route added since the caller's initial config read. +replace_config("port=10000\nwebsockets_/test/ws-555=first\n". + "websockets_/test/ws-556=second\n"); +my %added; +main::get_miniserv_config(\%added); +is($added{'websockets_/test/ws-556'}, 'second', + 'same-second route addition is visible'); + +# Cleanup must not restore a route another backend has already removed. +replace_config("port=10000\nwebsockets_/test/ws-556=second\n"); +my %removed; +main::get_miniserv_config(\%removed); +ok(!exists($removed{'websockets_/test/ws-555'}), + 'same-second route removal is visible'); +is($removed{'websockets_/test/ws-556'}, 'second', + 'unrelated route survives cleanup'); +done_testing(); diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index e32e3399a..eb63e8e00 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -3083,7 +3083,9 @@ hash reference. =cut sub get_miniserv_config { -return &read_file_cached_with_stat(&get_miniserv_config_file(), $_[0]); +# WebSocket backends can change routes several times within one second. +# An mtime-based cache can lose another process's update even under a file lock. +return &read_file(&get_miniserv_config_file(), $_[0]); } =head2 put_miniserv_config(&hash)