From c714be55b1490dff65f3977ced7204c771c7bf6d Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 8 Sep 2026 23:38:38 +0500 Subject: [PATCH] http: reject null or whitespace trigger event details --- src/Ramstack.HtmxToolkit/HtmxResponse.cs | 10 ++++---- src/Ramstack.HtmxToolkit/PendingEvents.cs | 18 +++++++------- .../PendingEventsTests.cs | 24 ++++++------------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs index a71726d..0db6007 100644 --- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs +++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs @@ -210,7 +210,7 @@ public HtmxResponse Reselect(string value) => /// See PR #3900. /// public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming trigger = HtmxTriggerTiming.Receive) => - QueueEvent(this, eventName, "{}", trigger); + AddToPendingEvent(this, eventName, "{}", trigger); /// /// Adds a client-side event and its detail to the response header selected by @@ -274,10 +274,10 @@ private static HtmxResponse SetHeader(HtmxResponse response, string key, string [RequiresDynamicCode("Event details are serialized using reflection.")] [RequiresUnreferencedCode("Event details are serialized using reflection.")] private static HtmxResponse TriggerEventCore(HtmxResponse response, string eventName, object detail, HtmxTriggerTiming timing) => - QueueEvent(response, eventName, JsonSerializer.Serialize(detail, JsonOptions.CamelCase), timing); + AddToPendingEvent(response, eventName, JsonSerializer.Serialize(detail, JsonOptions.CamelCase), timing); private static HtmxResponse TriggerEventCore(HtmxResponse response, string eventName, T detail, JsonTypeInfo jsonTypeInfo, HtmxTriggerTiming timing) => - QueueEvent(response, eventName, SerializeEventDetail(detail, jsonTypeInfo), timing); + AddToPendingEvent(response, eventName, SerializeEventDetail(detail, jsonTypeInfo), timing); private static string SerializeEventDetail(T detail, JsonTypeInfo jsonTypeInfo) { @@ -290,9 +290,9 @@ private static string SerializeEventDetail(T detail, JsonTypeInfo jsonType return Encoding.UTF8.GetString(buffer.WrittenSpan); } - private static HtmxResponse QueueEvent(HtmxResponse response, string eventName, string detail, HtmxTriggerTiming timing) + private static HtmxResponse AddToPendingEvent(HtmxResponse response, string eventName, string detailJson, HtmxTriggerTiming timing) { - PendingEvents.GetOrCreate(response._response).AddEvent(timing, eventName, detail); + PendingEvents.GetOrCreate(response._response).AddEvent(timing, eventName, detailJson); return response; } diff --git a/src/Ramstack.HtmxToolkit/PendingEvents.cs b/src/Ramstack.HtmxToolkit/PendingEvents.cs index 8020a5b..04470c5 100644 --- a/src/Ramstack.HtmxToolkit/PendingEvents.cs +++ b/src/Ramstack.HtmxToolkit/PendingEvents.cs @@ -41,22 +41,20 @@ private PendingEvents(HttpResponse response) => /// /// The time at which to trigger the events. /// The event name. - /// The event detail. - public void AddEvent(HtmxTriggerTiming timing, string eventName, string? detail) + /// The event detail as JSON. + public void AddEvent(HtmxTriggerTiming timing, string eventName, string detailJson) { - ArgumentNullException.ThrowIfNull(eventName); - if (string.IsNullOrWhiteSpace(eventName)) - throw new ArgumentException("Event name cannot be empty.", nameof(eventName)); + throw new ArgumentException("Event name cannot be null or whitespace.", nameof(eventName)); + + if (string.IsNullOrWhiteSpace(detailJson)) + throw new ArgumentException("Event detail JSON cannot be null or whitespace.", nameof(detailJson)); if (eventName == ProxyEventName) throw new ArgumentException( $"The event name '{ProxyEventName}' is reserved.", nameof(eventName)); - if (string.IsNullOrWhiteSpace(detail)) - detail = "{}"; - timing = NormalizeTiming(timing); var current = timing switch @@ -66,7 +64,7 @@ public void AddEvent(HtmxTriggerTiming timing, string eventName, string? detail) _ => _afterSettle ??= new SmallDictionary(StringComparer.Ordinal) }; - if (!current.TryAdd(eventName, detail)) + if (!current.TryAdd(eventName, detailJson)) { if (!current.TryGetValue(ProxyEventName, out var value) || value is not List> collection) @@ -75,7 +73,7 @@ public void AddEvent(HtmxTriggerTiming timing, string eventName, string? detail) current[ProxyEventName] = collection; } - collection.Add(new KeyValuePair(eventName, detail)); + collection.Add(new KeyValuePair(eventName, detailJson)); } } diff --git a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs index aa49ab5..bf70255 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs @@ -204,37 +204,27 @@ public void Flush_SerializesNullEventDetail_AsNull() [TestCase(null)] [TestCase("")] [TestCase(" ")] - public void AddEvent_NullOrWhitespaceDetail_NormalizesToEmptyObject(string? detail) + public void AddEvent_NullOrWhitespaceDetail_ThrowsArgumentException(string? detail) { var context = TestHelper.CreateHttpContext(); var pending = PendingEvents.GetOrCreate(context.Response); - pending.AddEvent(HtmxTriggerTiming.Receive, "e", detail); - pending.Flush(); - - var header = context.Response.Headers[HtmxResponseHeaderNames.Trigger].ToString(); - Assert.That(header, Is.EqualTo("{\"e\":{}}")); - } - - [Test] - public void AddEvent_NullEventName_ThrowsArgumentNullException() - { - var context = TestHelper.CreateHttpContext(); - var pending = PendingEvents.GetOrCreate(context.Response); + var exception = Assert.Throws(() => + pending.AddEvent(HtmxTriggerTiming.Receive, "e", detail!)); - Assert.Throws(() => - pending.AddEvent(HtmxTriggerTiming.Receive, null!, "1")); + Assert.That(exception?.ParamName, Is.EqualTo("detailJson")); } + [TestCase(null)] [TestCase("")] [TestCase(" ")] - public void AddEvent_EmptyOrWhitespaceEventName_ThrowsArgumentException(string eventName) + public void AddEvent_NullOrWhitespaceEventName_ThrowsArgumentException(string? eventName) { var context = TestHelper.CreateHttpContext(); var pending = PendingEvents.GetOrCreate(context.Response); var exception = Assert.Throws(() => - pending.AddEvent(HtmxTriggerTiming.Receive, eventName, "1")); + pending.AddEvent(HtmxTriggerTiming.Receive, eventName!, "1")); Assert.That(exception?.ParamName, Is.EqualTo("eventName")); }