Use toEncodedUrlPiece directly when encoding captures

Current implementation of captures uses the `toUrlPiece` method from the
`ToHttpApiData` typeclass, and encodes the resulting `Text` using `toEncodedUrlPiece`
when appending to the request path.

The problem with this approach is that the instance for `Text` percent-encodes
characters that are perfectly valid in URLs, such as `*`.

This patch makes direct use of `toEncodedUrlPiece`, which lets users implement
encoding according to their needs.

Closes #1511
This commit is contained in:
Gaël Deest 2022-03-21 17:29:23 +01:00
parent af3dde1b1d
commit 658217b021
4 changed files with 31 additions and 7 deletions

View file

@ -208,7 +208,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m api)
clientWithRoute pm (Proxy :: Proxy api)
(appendToPath p req)
where p = (toUrlPiece val)
where p = toEncodedUrlPiece val
hoistClientMonad pm _ f cl = \a ->
hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
@ -243,7 +243,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
clientWithRoute pm (Proxy :: Proxy sublayout)
(foldl' (flip appendToPath) req ps)
where ps = map (toUrlPiece) vals
where ps = map toEncodedUrlPiece vals
hoistClientMonad pm _ f cl = \as ->
hoistClientMonad pm (Proxy :: Proxy sublayout) f (cl as)
@ -740,7 +740,7 @@ instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
clientWithRoute pm (Proxy :: Proxy api)
(appendToPath p req)
where p = pack $ symbolVal (Proxy :: Proxy path)
where p = toEncodedUrlPiece $ pack $ symbolVal (Proxy :: Proxy path)
hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl

View file

@ -34,6 +34,8 @@ import Data.Bifunctor
import Data.Bitraversable
(Bitraversable (..), bifoldMapDefault, bimapDefault)
import qualified Data.ByteString as BS
import Data.ByteString.Builder
(Builder)
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Sequence as Seq
@ -112,7 +114,7 @@ instance (NFData path, NFData body) => NFData (RequestF body path) where
rnfB Nothing = ()
rnfB (Just (b, mt)) = rnf b `seq` mediaTypeRnf mt
type Request = RequestF RequestBody Builder.Builder
type Request = RequestF RequestBody Builder
-- | The request body. R replica of the @http-client@ @RequestBody@.
data RequestBody
@ -145,9 +147,10 @@ defaultRequest = Request
-- | Append extra path to the request being constructed.
--
appendToPath :: Text -> Request -> Request
-- Warning: This function assumes that the path fragment is already URL-encoded.
appendToPath :: Builder -> Request -> Request
appendToPath p req
= req { requestPath = requestPath req <> "/" <> toEncodedUrlPiece p }
= req { requestPath = requestPath req <> "/" <> p }
-- | Append a query parameter to the request being constructed.
--

View file

@ -160,6 +160,7 @@ type Api =
WithStatus 301 Text]
:<|> "uverb-get-created" :> UVerb 'GET '[PlainText] '[WithStatus 201 Person]
:<|> NamedRoutes RecordRoutes
:<|> "captureVerbatim" :> Capture "someString" Verbatim :> Get '[PlainText] Text
api :: Proxy Api
api = Proxy
@ -214,7 +215,8 @@ getRoot
:<|> EmptyClient
:<|> uverbGetSuccessOrRedirect
:<|> uverbGetCreated
:<|> recordRoutes = client api
:<|> recordRoutes
:<|> captureVerbatim = client api
server :: Application
server = serve api (
@ -259,6 +261,7 @@ server = serve api (
{ something = pure ["foo", "bar", "pweet"]
}
}
:<|> pure . decodeUtf8 . unVerbatim
)
-- * api for testing failures
@ -370,3 +373,12 @@ instance ToHttpApiData UrlEncodedByteString where
instance FromHttpApiData UrlEncodedByteString where
parseUrlPiece = pure . UrlEncodedByteString . HTTP.urlDecode True . encodeUtf8
newtype Verbatim = Verbatim { unVerbatim :: ByteString }
instance ToHttpApiData Verbatim where
toEncodedUrlPiece = byteString . unVerbatim
toUrlPiece = decodeUtf8 . unVerbatim
instance FromHttpApiData Verbatim where
parseUrlPiece = pure . Verbatim . encodeUtf8

View file

@ -36,6 +36,8 @@ import Data.Maybe
import Data.Monoid ()
import Data.Text
(Text)
import Data.Text.Encoding
(encodeUtf8)
import qualified Network.HTTP.Client as C
import qualified Network.HTTP.Types as HTTP
import Test.Hspec
@ -196,3 +198,10 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
case eitherResponse of
Left clientError -> fail $ show clientError
Right response -> matchUnion response `shouldBe` Just (WithStatus @201 carol)
it "encodes URL pieces following ToHttpApiData instance" $ \(_, baseUrl) -> do
let textOrig = "*"
eitherResponse <- runClient (captureVerbatim $ Verbatim $ encodeUtf8 textOrig) baseUrl
case eitherResponse of
Left clientError -> fail $ show clientError
Right textBack -> textBack `shouldBe` textOrig