Proposed changes to web-routes: query-string support

Hi Jeremy, I've CCed the web-devel list, as I think the discussion will be of interest to a number of readers there. When we were originally working on web-routes, you had mentioned the idea of including some form of support for query strings, and I said it was a bad idea. I'd like to backtrack on that statement: I *would* like to include support, and I'm including a patch that does just that. Let me explain the use case this is meant to serve, which will help explain my design decisions. Yesod includes a static file subsite. One trick a lot of sites are using these days is including a hash of the file contents in the query string of the URL and setting the expiration date of the file far in the future. This way, as long as the file contents stay the same, the browser should use the cached copy, and as soon as a new version is available, the HTML will reflect this by a change in the hash value. I wanted to provide this feature with Yesod, but realized there was no straight-forward way to do this. What I needed was to change the formatPathSegments function to have a type signature of: url -> ([String], [(String, String)]), where the second element in the tuple is the query-string parameters. This, however, presents a small problem: let's say that we want to add query string parameters *on top of* those provided by the formatPathSegments function; we could do manually addition textually, but we won't know whether to include is as "?foo=bar" or "&foo=bar"; the former should be used if formatPathSegments does not given any query string parameter, the latter if it does. I therefore modified the handleSite function to be: (url -> [(String, String)] -> String) -> url -> a. I also changed encodePathInfo to also encode the query string parameters, so its type is now: [String] -> [(String, String)] -> String. All the other changes were just to get the code to compile. I'm planning on this being a change throughout a few other libraries: web-routes-quasi will obviously change to reflect this, as will Yesod. One other perhaps unexpected change would be in Hamlet: the definition of Hamlet is now "type Hamlet = (url -> [(String, String)] -> String) -> Html ()". One nice side effect of this is query-string encoding code is now completely localized to web-routes, instead of copied throughout multiple packages. This at least implies to me that this is a good move. You'll notice that I have purposely *not* modified decodePathInfo and parsePathSegments; I still don't believe routing should be affected in any way by query string parameters. Look forward to hearing your thoughts on this, Michael

Nice! I have not had time to look at this in detail, and probably won't for a few more days. But I am certainly in favor of the concept. I recently wrote some code for one of my own projects to deal with creating query string key/value pairs. It will be interesting to see how they overlap. - jeremy p.s. Also, the query string is not required to be key/value pairs. It can be any non-hierarchical data which, combined with the path info, serves to identify a resource. It just happens the key/value pairs are the single most common what of encoding the this data. If web-routes is going to only support key/value pairs, that is fine. But we should probably acknowledge that in the docs. On Aug 8, 2010, at 7:03 AM, Michael Snoyman wrote:
Hi Jeremy,
I've CCed the web-devel list, as I think the discussion will be of interest to a number of readers there.
When we were originally working on web-routes, you had mentioned the idea of including some form of support for query strings, and I said it was a bad idea. I'd like to backtrack on that statement: I *would* like to include support, and I'm including a patch that does just that. Let me explain the use case this is meant to serve, which will help explain my design decisions.
Yesod includes a static file subsite. One trick a lot of sites are using these days is including a hash of the file contents in the query string of the URL and setting the expiration date of the file far in the future. This way, as long as the file contents stay the same, the browser should use the cached copy, and as soon as a new version is available, the HTML will reflect this by a change in the hash value.
I wanted to provide this feature with Yesod, but realized there was no straight-forward way to do this. What I needed was to change the formatPathSegments function to have a type signature of: url -> ([String], [(String, String)]), where the second element in the tuple is the query-string parameters. This, however, presents a small problem: let's say that we want to add query string parameters *on top of* those provided by the formatPathSegments function; we could do manually addition textually, but we won't know whether to include is as "?foo=bar" or "&foo=bar"; the former should be used if formatPathSegments does not given any query string parameter, the latter if it does.
I therefore modified the handleSite function to be: (url -> [(String, String)] -> String) -> url -> a. I also changed encodePathInfo to also encode the query string parameters, so its type is now: [String] -> [(String, String)] -> String. All the other changes were just to get the code to compile.
I'm planning on this being a change throughout a few other libraries: web-routes-quasi will obviously change to reflect this, as will Yesod. One other perhaps unexpected change would be in Hamlet: the definition of Hamlet is now "type Hamlet = (url -> [(String, String)] -> String) -> Html ()". One nice side effect of this is query-string encoding code is now completely localized to web-routes, instead of copied throughout multiple packages. This at least implies to me that this is a good move.
You'll notice that I have purposely *not* modified decodePathInfo and parsePathSegments; I still don't believe routing should be affected in any way by query string parameters.
Look forward to hearing your thoughts on this, Michael

Jeremy Shaw
Nice!
I have not had time to look at this in detail, and probably won't for a few more days. But I am certainly in favor of the concept.
I recently wrote some code for one of my own projects to deal with creating query string key/value pairs. It will be interesting to see how they overlap.
- jeremy
p.s. Also, the query string is not required to be key/value pairs. It can be any non-hierarchical data which, combined with the path info, serves to identify a resource. It just happens the key/value pairs are the single most common what of encoding the this data. If web-routes is going to only support key/value pairs, that is fine. But we should probably acknowledge that in the docs.
Should the type of the query string part should be "[(String,[String])]"
instead of "[(String, String)]"? I guess it depends on whether you
wanted "/?foo=a&foo=b&foo=c" to decode the query params as:
[("foo","a"), ("foo","b"), ("foo","c")]
vs
[("foo", ["a", "b", "c"])]
The latter has the advantage that "lookup" returns all of the values at
once. Would "Map String [String]" be better?
G
--
Gregory Collins

Jeremy Shaw
writes: Nice!
I have not had time to look at this in detail, and probably won't for a few more days. But I am certainly in favor of the concept.
I recently wrote some code for one of my own projects to deal with creating query string key/value pairs. It will be interesting to see how they overlap.
- jeremy
p.s. Also, the query string is not required to be key/value pairs. It can be any non-hierarchical data which, combined with the path info, serves to identify a resource. It just happens the key/value pairs are the single most common what of encoding the this data. If web-routes is going to only support key/value pairs, that is fine. But we should probably acknowledge that in the docs.
Should the type of the query string part should be "[(String,[String])]" instead of "[(String, String)]"? I guess it depends on whether you wanted "/?foo=a&foo=b&foo=c" to decode the query params as:
[("foo","a"), ("foo","b"), ("foo","c")]
vs
[("foo", ["a", "b", "c"])]
The latter has the advantage that "lookup" returns all of the values at once. Would "Map String [String]" be better?
I know that in practice we usually ignore the order of the keys, but in
On Mon, Aug 9, 2010 at 9:56 PM, Gregory Collins

On Mon, Aug 9, 2010 at 9:32 PM, Jeremy Shaw
Nice!
I have not had time to look at this in detail, and probably won't for a few more days. But I am certainly in favor of the concept.
I recently wrote some code for one of my own projects to deal with creating query string key/value pairs. It will be interesting to see how they overlap.
- jeremy
p.s. Also, the query string is not required to be key/value pairs. It can be any non-hierarchical data which, combined with the path info, serves to identify a resource. It just happens the key/value pairs are the single most common what of encoding the this data. If web-routes is going to only support key/value pairs, that is fine. But we should probably acknowledge that in the docs.
Sounds good, I agree.

I've added an extra patch into the bundle with just a minor optimization:
when a pair in the query string list has an empty value, it doesn't output
an equals sign. Nothing ground-breaking, but makes the URLs look nicer for
those cases.
Michael
On Mon, Aug 9, 2010 at 9:32 PM, Jeremy Shaw
Nice!
I have not had time to look at this in detail, and probably won't for a few more days. But I am certainly in favor of the concept.
I recently wrote some code for one of my own projects to deal with creating query string key/value pairs. It will be interesting to see how they overlap.
- jeremy
p.s. Also, the query string is not required to be key/value pairs. It can be any non-hierarchical data which, combined with the path info, serves to identify a resource. It just happens the key/value pairs are the single most common what of encoding the this data. If web-routes is going to only support key/value pairs, that is fine. But we should probably acknowledge that in the docs.
On Aug 8, 2010, at 7:03 AM, Michael Snoyman wrote:
Hi Jeremy,
I've CCed the web-devel list, as I think the discussion will be of interest to a number of readers there.
When we were originally working on web-routes, you had mentioned the idea of including some form of support for query strings, and I said it was a bad idea. I'd like to backtrack on that statement: I *would* like to include support, and I'm including a patch that does just that. Let me explain the use case this is meant to serve, which will help explain my design decisions.
Yesod includes a static file subsite. One trick a lot of sites are using these days is including a hash of the file contents in the query string of the URL and setting the expiration date of the file far in the future. This way, as long as the file contents stay the same, the browser should use the cached copy, and as soon as a new version is available, the HTML will reflect this by a change in the hash value.
I wanted to provide this feature with Yesod, but realized there was no straight-forward way to do this. What I needed was to change the formatPathSegments function to have a type signature of: url -> ([String], [(String, String)]), where the second element in the tuple is the query-string parameters. This, however, presents a small problem: let's say that we want to add query string parameters *on top of* those provided by the formatPathSegments function; we could do manually addition textually, but we won't know whether to include is as "?foo=bar" or "&foo=bar"; the former should be used if formatPathSegments does not given any query string parameter, the latter if it does.
I therefore modified the handleSite function to be: (url -> [(String, String)] -> String) -> url -> a. I also changed encodePathInfo to also encode the query string parameters, so its type is now: [String] -> [(String, String)] -> String. All the other changes were just to get the code to compile.
I'm planning on this being a change throughout a few other libraries: web-routes-quasi will obviously change to reflect this, as will Yesod. One other perhaps unexpected change would be in Hamlet: the definition of Hamlet is now "type Hamlet = (url -> [(String, String)] -> String) -> Html ()". One nice side effect of this is query-string encoding code is now completely localized to web-routes, instead of copied throughout multiple packages. This at least implies to me that this is a good move.
You'll notice that I have purposely *not* modified decodePathInfo and parsePathSegments; I still don't believe routing should be affected in any way by query string parameters.
Look forward to hearing your thoughts on this, Michael

Hello,
There are a few concerns I have with the patch so far:
1. When a form uses method="GET", the searchpart (query string) of
the action is ignored. For example, if you do:
<form action="/get?foo=bar" method="GET">...</form>
The, foo=bar, will not appear in the query string when the form is
submitted. I guess there is really nothing we can do about this
except document the fact that you shouldn't do that..
2. I think the encodeUrlChar function is too aggressive. It seems to
encode all reserved characters, not just the ones that have special
meaning?
"Reserved characters are those characters that *sometimes* have
special meaning"
"The sets of reserved and unreserved characters and the circumstances
under which certain reserved characters have special meaning have
changed slightly with each revision of specifications that govern URIs
and URI schemes."
"When a character from the reserved set (a "reserved character") has
special meaning (a "reserved purpose") in a certain context, and a URI
scheme says that it is necessary to use that character for some other
purpose, then the character must be percent-encoded."
I believe that :@$, do not need to be encoded in the query string?
3. integration with other parts of web-routes
It looks like your patch handles integration with things like RouteT
by simply hardcoding the query parameters to the empty list. If we are
going to add support for this, then it would be nice if it was
actually usable everywhere. I will see what happens when I try to add
support I guess.
- jeremy
On Sun, Aug 8, 2010 at 7:03 AM, Michael Snoyman
Hi Jeremy, I've CCed the web-devel list, as I think the discussion will be of interest to a number of readers there. When we were originally working on web-routes, you had mentioned the idea of including some form of support for query strings, and I said it was a bad idea. I'd like to backtrack on that statement: I *would* like to include support, and I'm including a patch that does just that. Let me explain the use case this is meant to serve, which will help explain my design decisions. Yesod includes a static file subsite. One trick a lot of sites are using these days is including a hash of the file contents in the query string of the URL and setting the expiration date of the file far in the future. This way, as long as the file contents stay the same, the browser should use the cached copy, and as soon as a new version is available, the HTML will reflect this by a change in the hash value. I wanted to provide this feature with Yesod, but realized there was no straight-forward way to do this. What I needed was to change the formatPathSegments function to have a type signature of: url -> ([String], [(String, String)]), where the second element in the tuple is the query-string parameters. This, however, presents a small problem: let's say that we want to add query string parameters *on top of* those provided by the formatPathSegments function; we could do manually addition textually, but we won't know whether to include is as "?foo=bar" or "&foo=bar"; the former should be used if formatPathSegments does not given any query string parameter, the latter if it does. I therefore modified the handleSite function to be: (url -> [(String, String)] -> String) -> url -> a. I also changed encodePathInfo to also encode the query string parameters, so its type is now: [String] -> [(String, String)] -> String. All the other changes were just to get the code to compile. I'm planning on this being a change throughout a few other libraries: web-routes-quasi will obviously change to reflect this, as will Yesod. One other perhaps unexpected change would be in Hamlet: the definition of Hamlet is now "type Hamlet = (url -> [(String, String)] -> String) -> Html ()". One nice side effect of this is query-string encoding code is now completely localized to web-routes, instead of copied throughout multiple packages. This at least implies to me that this is a good move. You'll notice that I have purposely *not* modified decodePathInfo and parsePathSegments; I still don't believe routing should be affected in any way by query string parameters. Look forward to hearing your thoughts on this, Michael

Regarding the query string encoding, this is what I have used in other
code and am likely to use in web-routes, unless it is wrong:
paramsToQueryString :: [(String, String)] -> String
paramsToQueryString [] = ""
paramsToQueryString ps = '?' : concat (intersperse "&" (map
paramToQueryString ps))
where
isOK :: Char -> Bool
isOK c = isUnreserved c || (c `elem` ":@$,")
escapeParamChar :: Char -> String
escapeParamChar ' ' = "+"
escapeParamChar c = escapeURIChar isOK c
escapeParamString :: String -> String
escapeParamString = concatMap escapeParamChar
paramToQueryString :: (String, String) -> String
paramToQueryString (k,v) = (escapeParamString k) ++ ('=' :
escapeParamString v)
- jeremy
On Tue, Sep 21, 2010 at 2:09 PM, Jeremy Shaw
Hello,
There are a few concerns I have with the patch so far:
1. When a form uses method="GET", the searchpart (query string) of the action is ignored. For example, if you do:
<form action="/get?foo=bar" method="GET">...</form>
The, foo=bar, will not appear in the query string when the form is submitted. I guess there is really nothing we can do about this except document the fact that you shouldn't do that..
2. I think the encodeUrlChar function is too aggressive. It seems to encode all reserved characters, not just the ones that have special meaning?
"Reserved characters are those characters that *sometimes* have special meaning"
"The sets of reserved and unreserved characters and the circumstances under which certain reserved characters have special meaning have changed slightly with each revision of specifications that govern URIs and URI schemes."
"When a character from the reserved set (a "reserved character") has special meaning (a "reserved purpose") in a certain context, and a URI scheme says that it is necessary to use that character for some other purpose, then the character must be percent-encoded."
I believe that :@$, do not need to be encoded in the query string?
3. integration with other parts of web-routes
It looks like your patch handles integration with things like RouteT by simply hardcoding the query parameters to the empty list. If we are going to add support for this, then it would be nice if it was actually usable everywhere. I will see what happens when I try to add support I guess.
- jeremy
On Sun, Aug 8, 2010 at 7:03 AM, Michael Snoyman
wrote: Hi Jeremy, I've CCed the web-devel list, as I think the discussion will be of interest to a number of readers there. When we were originally working on web-routes, you had mentioned the idea of including some form of support for query strings, and I said it was a bad idea. I'd like to backtrack on that statement: I *would* like to include support, and I'm including a patch that does just that. Let me explain the use case this is meant to serve, which will help explain my design decisions. Yesod includes a static file subsite. One trick a lot of sites are using these days is including a hash of the file contents in the query string of the URL and setting the expiration date of the file far in the future. This way, as long as the file contents stay the same, the browser should use the cached copy, and as soon as a new version is available, the HTML will reflect this by a change in the hash value. I wanted to provide this feature with Yesod, but realized there was no straight-forward way to do this. What I needed was to change the formatPathSegments function to have a type signature of: url -> ([String], [(String, String)]), where the second element in the tuple is the query-string parameters. This, however, presents a small problem: let's say that we want to add query string parameters *on top of* those provided by the formatPathSegments function; we could do manually addition textually, but we won't know whether to include is as "?foo=bar" or "&foo=bar"; the former should be used if formatPathSegments does not given any query string parameter, the latter if it does. I therefore modified the handleSite function to be: (url -> [(String, String)] -> String) -> url -> a. I also changed encodePathInfo to also encode the query string parameters, so its type is now: [String] -> [(String, String)] -> String. All the other changes were just to get the code to compile. I'm planning on this being a change throughout a few other libraries: web-routes-quasi will obviously change to reflect this, as will Yesod. One other perhaps unexpected change would be in Hamlet: the definition of Hamlet is now "type Hamlet = (url -> [(String, String)] -> String) -> Html ()". One nice side effect of this is query-string encoding code is now completely localized to web-routes, instead of copied throughout multiple packages. This at least implies to me that this is a good move. You'll notice that I have purposely *not* modified decodePathInfo and parsePathSegments; I still don't believe routing should be affected in any way by query string parameters. Look forward to hearing your thoughts on this, Michael
participants (3)
-
Gregory Collins
-
Jeremy Shaw
-
Michael Snoyman