diff --git a/http.carp b/http.carp index 5d4f117..8326023 100644 --- a/http.carp +++ b/http.carp @@ -1,7 +1,7 @@ (load "git@github.com:carpentry-org/base64.carp@0.2.0") -(load "git@github.com:carpentry-org/uri@0.1.0") -(load "git@github.com:carpentry-org/time@0.5.0") -(load "git@github.com:carpentry-org/strbuf@0.1.0") +(load "git@github.com:carpentry-org/uri@0.5.1") +(load "git@github.com:carpentry-org/time@0.5.2") +(load "git@github.com:carpentry-org/strbuf@0.2.1") (defmodule String (doc find-crlf @@ -310,12 +310,25 @@ attribute, whatever order the two appear in.") (doc ignore-body? "checks whether the body of the request should be ignored.") (defn ignore-body? [r] (= (verb r) "HEAD")) + (private target) + (hidden target) + ; RFC 9112 §3.2.1: an origin-form target with an empty path is sent as `/`. + (defn target [u] + (let [s (URI.str u)] + (if (and (Maybe.nothing? (URI.path u)) + (Maybe.nothing? (URI.scheme u)) + (Maybe.nothing? (URI.host u)) + (Maybe.nothing? (URI.user u)) + (Maybe.nothing? (URI.port u))) + (String.append "/" &s) + s))) + (doc str "stringifies the request as it looks on the wire.") (defn str [r] (let-do [sb (StringBuf.create)] (StringBuf.append-str &sb (verb r)) (StringBuf.append-char &sb \space) - (StringBuf.append-str &sb &(URI.str (uri r))) + (StringBuf.append-str &sb &(target (uri r))) (StringBuf.append-char &sb \space) (StringBuf.append-str &sb (version r)) (let [ks &(Map.keys (headers r))] diff --git a/test/http.carp b/test/http.carp index d75041d..9743229 100644 --- a/test/http.carp +++ b/test/http.carp @@ -413,6 +413,29 @@ "GET" "request roundtrip preserves verb") + (assert-equal test + "GET / HTTP/1.1\r\n\r\n" + &(Request.str &(Request.get (URI.zero) [] {} @"")) + "request-target of an empty path is /") + + (assert-equal test + "GET /?a=1 HTTP/1.1\r\n\r\n" + &(Request.str + &(Request.get (URI.set-query (URI.zero) (Maybe.Just @"a=1")) [] {} @"")) + "request-target of a query-only URI keeps its leading /") + + (assert-equal test + "GET http://example.com HTTP/1.1\r\n\r\n" + &(Request.str + &(Request.get + (URI.set-scheme + (URI.set-host (URI.zero) (Maybe.Just @"example.com")) + (Maybe.Just @"http")) + [] + {} + @"")) + "absolute-form request-target is left alone") + (assert-true test (match (Cookie.parse "name=value") (Result.Success c) @@ -816,11 +839,11 @@ "encode & as %26") (assert-equal test - "k=a%2bb" + "k=a%2Bb" &(let-do [m (the (Map String String) {})] (Map.put! &m "k" "a+b") (Form.encode &m)) - "encode literal + as %2b") + "encode literal + as %2B") (assert-equal test "key+name=val" @@ -838,6 +861,37 @@ _ false)) "encode/parse roundtrip single pair") + (assert-equal test + "k=%E2%82%AC" + &(let-do [m (the (Map String String) {})] + (Map.put! &m "k" "€") + (Form.encode &m)) + "encode a non-ASCII value one byte at a time") + + (assert-equal test + "€" + &(let-do [m (the (Map String String) {})] + (Map.put! &m "k" "€") + (match (the (Result (Map String String) String) + (Form.parse &(Form.encode &m))) + (Result.Success parsed) (Map.get &parsed "k") + _ @"")) + "encode/parse roundtrip a non-ASCII value") + + (assert-equal test + "b%4" + &(match (the (Result (Map String String) String) (Form.parse "k=b%4")) + (Result.Success m) (Map.get &m "k") + _ @"") + "parse a value ending in a truncated percent escape") + + (assert-equal test + "b%" + &(match (the (Result (Map String String) String) (Form.parse "k=b%")) + (Result.Success m) (Map.get &m "k") + _ @"") + "parse a value ending in a bare percent") + (assert-equal test 2 (match (the