Skip to content

Inconsistent query parameter handling/encoding between server and client #1779

Open
@zlondrej

Description

@zlondrej

The query parameter URL-encoding has undocumented behavior. I was trying to write some HasServer and HasClient instances working with Query and oh boy, what it took me a while to get out of that rabbit hole.

servant-server is based on wai which uses parseQuery (https://github.com/yesodweb/wai/blob/master/warp/Network/Wai/Handler/Warp/Request.hs#L108). This will always properly decode both query key and value.

However the servant-client introduced a risky approach to client parameter encoding when #1432 was merged (and doesn't even seems necessary). Now only query keys are implicitly encoded. QueryParam' and QueryParams seem to be aware of this (after some bugfixes), and explicitly encode the value using encodeQueryParam (QueryParam' and QueryParams instances).

However since then, few more query handling types were introduced and they seem to be unaware of the fact. Namely QueryString (the main issue here is undocumented behavior - both server and client-vise, developers won't know what to expect while using it) and DeepQuery (no URL-encoding is performed here either).

There have been few issues/PRs related to URL-encoding reported in the past:

Now there's a proposed solution based on fizruk/http-api-data#120 (toEncodedQueryParam) that is attempting somewhat fixing the symptoms:

But the source of the issue is still there. There's nothing saying whether the requestQueryString :: Query in the client's `Request' should or should not be URL-encoded (be it documentation or the types).


Proposal

I think the surefire solution would be to switch to PartialEscapeQuery for servant-client, which forces selection of URL-encoding on the type level and encoding it using renderQueryPartialEscape.

Such change will introduce an API incompatibility with older versions, but that's actually for the best as it will force everyone to review their implementations. Also the migration should be quite easy, wrapping any existing values into list of QE's or QN's (EscapeItem's constructors) and replacing Nothing with empty list. Introducting some smart constructors can make it even easier.

And for servant-server documenting that QueryString returns URL-decoded keys and values would also be a good idea. Not sure whether anything more can be done. Wrapping value in something like newtype UrlDecodedQueryValue = QD ByteString (names are just example, not a suggestion) to give the info on the type level seems to be excessive. Maybe a documented type alias like type DecodedQuery = Query would not hurt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions