From ecfb7e82aa128f57523cf783e7993cd3415a2097 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Tue, 14 Jul 2026 09:29:41 +0200 Subject: [PATCH] Fix Form.parse and Form.parse-pairs on an empty body An empty urlencoded body has no fields, but both parsers relied on (String.split-by "" &[\&]) which returns a single empty element, so an empty input produced a spurious one-entry result: Form.parse "" yielded {"": ""} and Form.parse-pairs "" yielded [("", "")]. Guard empty input at the top of both functions, returning an empty map/array immediately (mirroring the empty-input handling in the Accept and CacheControl parsers). All non-empty behaviour, including a bare key with no '=', is unchanged. Fixes the previously-failing 'form-data on empty body returns empty map' suite test and adds a regression test for the parse-pairs case. --- http.carp | 52 +++++++++++++++++++++++++++----------------------- test/http.carp | 9 +++++++++ 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/http.carp b/http.carp index e42f122..7540fa6 100644 --- a/http.carp +++ b/http.carp @@ -686,18 +686,20 @@ bad hex size or a truncated chunk.") Keys and values are percent-decoded. The `+` character is decoded as space per the `application/x-www-form-urlencoded` specification.") (defn parse [s] - (let-do [m (the (Map String String) {}) - pairs &(String.split-by s &[\&])] - (for [i 0 (Array.length pairs)] - (let [pair (Array.unsafe-nth pairs i) - eq (String.index-of pair \=)] - (if (= eq -1) - (let [k (form-unescape pair)] (Map.put! &m &k "")) - (let [k (form-unescape &(String.byte-slice pair 0 eq)) - v (form-unescape - &(String.byte-slice pair (Int.inc eq) (String.length pair)))] - (Map.put! &m &k &v))))) - (Result.Success m))) + (if (String.empty? s) + (Result.Success (the (Map String String) {})) + (let-do [m (the (Map String String) {}) + pairs &(String.split-by s &[\&])] + (for [i 0 (Array.length pairs)] + (let [pair (Array.unsafe-nth pairs i) + eq (String.index-of pair \=)] + (if (= eq -1) + (let [k (form-unescape pair)] (Map.put! &m &k "")) + (let [k (form-unescape &(String.byte-slice pair 0 eq)) + v (form-unescape + &(String.byte-slice pair (Int.inc eq) (String.length pair)))] + (Map.put! &m &k &v))))) + (Result.Success m)))) (private form-escape) (hidden form-escape) @@ -724,18 +726,20 @@ This is the inverse of `parse`.") of pairs and allows duplicate keys. Keys and values are percent-decoded. The `+` character is decoded as space.") (defn parse-pairs [s] - (let-do [result (the (Array (Pair String String)) []) - parts &(String.split-by s &[\&])] - (for [i 0 (Array.length parts)] - (let [part (Array.unsafe-nth parts i) - eq (String.index-of part \=)] - (if (= eq -1) - (Array.push-back! &result (Pair.init (form-unescape part) @"")) - (let [k (form-unescape &(String.byte-slice part 0 eq)) - v (form-unescape - &(String.byte-slice part (Int.inc eq) (String.length part)))] - (Array.push-back! &result (Pair.init k v)))))) - (Result.Success result)))) + (if (String.empty? s) + (Result.Success (the (Array (Pair String String)) [])) + (let-do [result (the (Array (Pair String String)) []) + parts &(String.split-by s &[\&])] + (for [i 0 (Array.length parts)] + (let [part (Array.unsafe-nth parts i) + eq (String.index-of part \=)] + (if (= eq -1) + (Array.push-back! &result (Pair.init (form-unescape part) @"")) + (let [k (form-unescape &(String.byte-slice part 0 eq)) + v (form-unescape + &(String.byte-slice part (Int.inc eq) (String.length part)))] + (Array.push-back! &result (Pair.init k v)))))) + (Result.Success result))))) (doc MediaType "is a parsed media type — the value of a `Content-Type` header, such as `multipart/form-data; boundary=abc`. `type` and `subtype` are diff --git a/test/http.carp b/test/http.carp index d9c592c..b1eb883 100644 --- a/test/http.carp +++ b/test/http.carp @@ -549,6 +549,15 @@ (Result.Success ps) (= (Pair.b (Array.unsafe-nth &ps 0)) "&=") _ false) "parse-pairs decodes percent-encoded values") + + (assert-equal test + 0 + (match (the + (Result (Array (Pair String String)) String) + (Form.parse-pairs "")) + (Result.Success ps) (Array.length &ps) + _ -1) + "parse-pairs on empty body returns empty array") ; ---- MediaType ---- (assert-equal test "multipart/form-data"