Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/Request.html
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,14 @@ <h3 id="str">
</pre>
<p class="doc">
<p>stringifies the request as it looks on the wire.</p>
<p>The request-target is rendered in origin-form: the path and query of the
request’s URI, or <code>/</code> when it has neither, as RFC 9112 §3.2.1 requires of a
request sent directly to an origin server. Scheme, host, port, userinfo and
fragment stay on the URI and never reach the request line, so the authority has
to travel in a <code>Host</code> header.</p>
<p>The other request-target forms of RFC 9112 §3.2 are not produced:
absolute-form addresses a proxy, authority-form a <code>CONNECT</code>, and asterisk-form
a server-wide <code>OPTIONS</code>.</p>

</p>
</div>
Expand Down
35 changes: 20 additions & 15 deletions http.carp
Original file line number Diff line number Diff line change
Expand Up @@ -312,22 +312,27 @@ attribute, whatever order the two appear in.")

(private target)
(hidden target)
; RFC 9110 §4.2.4: a request target never carries userinfo.
; RFC 9110 §7.1: the target URI excludes the fragment component.
; RFC 9112 §3.2.1: an origin-form target with an empty path is sent as `/`.
; RFC 9112 §3.2.1: a request to an origin server carries path and query only.
(defn target [u]
(let [v (URI.set-fragment
(URI.set-password (URI.set-user @u (Maybe.Nothing)) (Maybe.Nothing))
(Maybe.Nothing))
s (URI.str &v)]
(if (and (Maybe.nothing? (URI.path &v))
(Maybe.nothing? (URI.scheme &v))
(Maybe.nothing? (URI.host &v))
(Maybe.nothing? (URI.port &v)))
(String.append "/" &s)
s)))

(doc str "stringifies the request as it looks on the wire.")
(let [pth (Maybe.from @(URI.path u) @"")
q (Maybe.from @(URI.query u) @"")]
(String.concat
&[@(if (String.byte-starts-with? &pth "/") "" "/")
pth
@(if (String.empty? &q) "" "?")
q])))

(doc str "stringifies the request as it looks on the wire.

The request-target is rendered in origin-form: the path and query of the
request’s URI, or `/` when it has neither, as RFC 9112 §3.2.1 requires of a
request sent directly to an origin server. Scheme, host, port, userinfo and
fragment stay on the URI and never reach the request line, so the authority has
to travel in a `Host` header.

The other request-target forms of RFC 9112 §3.2 are not produced:
absolute-form addresses a proxy, authority-form a `CONNECT`, and asterisk-form
a server-wide `OPTIONS`.")
(defn str [r]
(let-do [sb (StringBuf.create)]
(StringBuf.append-str &sb (verb r))
Expand Down
43 changes: 32 additions & 11 deletions test/http.carp
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@
"request-target of a query-only URI keeps its leading /")

(assert-equal test
"GET http://example.com HTTP/1.1\r\n\r\n"
"GET / HTTP/1.1\r\n\r\n"
&(Request.str
&(Request.get
(URI.set-scheme
Expand All @@ -482,22 +482,43 @@
[]
{}
@""))
"absolute-form request-target is left alone")
"request-target of an absolute URL drops the scheme and host")

(assert-equal test
"GET /a/b?q=1 HTTP/1.1\r\n\r\n"
&(request-line "http://user:pw@h.example:8080/a/b?q=1#sec2")
"request-target keeps only the path and query")

(assert-equal test
"GET /a/b HTTP/1.1\r\n\r\n"
&(Request.str
&(Request.get (URI.set-path (URI.zero) (Maybe.Just @"/a/b")) [] {} @""))
"an already absolute path is not slashed twice")

(assert-true test
(match (URI.parse "http://h.example:8080/a/b")
(Result.Success u)
(let [r (Request.get u [] {} @"")]
(and
(= &(Maybe.Just @"h.example") (URI.host (Request.uri &r)))
(= &(Maybe.Just 8080) (URI.port (Request.uri &r)))))
_ false)
"the host and port stay on the request’s URI")

(assert-equal test
"GET http://h.example/a/c HTTP/1.1\r\n\r\n"
"GET /a/c HTTP/1.1\r\n\r\n"
&(request-line "http://USER:PW@h.example/a/c")
"request-target drops user and password")

(assert-equal test
"GET http://h.example/a/c HTTP/1.1\r\n\r\n"
"GET /a/c HTTP/1.1\r\n\r\n"
&(request-line "http://USER@h.example/a/c")
"request-target drops a user without a password")

(assert-equal test
"GET http://h.example/a/c HTTP/1.1\r\n\r\n"
"GET /a/c HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/c")
"request-target without userinfo is unchanged")
"request-target without userinfo is the plain path")

(assert-equal test
"GET / HTTP/1.1\r\n\r\n"
Expand All @@ -516,27 +537,27 @@
"the credentials stay on the request’s URI")

(assert-equal test
"GET http://h.example/a/b?q=1 HTTP/1.1\r\n\r\n"
"GET /a/b?q=1 HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/b?q=1#sec2")
"request-target drops a fragment and keeps the query")

(assert-equal test
"GET http://h.example/a/b HTTP/1.1\r\n\r\n"
"GET /a/b HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/b#sec2")
"request-target drops a fragment without a query")

(assert-equal test
"GET http://h.example/a/b HTTP/1.1\r\n\r\n"
"GET /a/b HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/b#")
"request-target drops an empty fragment")

(assert-equal test
"GET http://h.example/a/b HTTP/1.1\r\n\r\n"
"GET /a/b HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/b#frag?notquery")
"request-target drops a fragment that looks like a query")

(assert-equal test
"GET http://h.example/a/b?q=a%23b HTTP/1.1\r\n\r\n"
"GET /a/b?q=a%23b HTTP/1.1\r\n\r\n"
&(request-line "http://h.example/a/b?q=a%23b")
"request-target keeps an encoded # inside the query")

Expand Down