Search code examples
httphaskellgetjira

Encoding problem with GET requests in Haskell


I'm trying to get some Json data from a Jira server using Haskell. I'm counting this as "me having problems with Haskell" rather than encodings or Jira because my problem is when doing this in Haskell.

The problem occurs when the URL (or query) has plus signs. After building my request for theproject+order+by+created, Haskell prints it as:

Request {
  host                 = "myjiraserver.com"
  port                 = 443
  secure               = True
  requestHeaders       = [("Content-Type","application/json"),("Authorization","<REDACTED>")]
  path                 = "/jira/rest/api/2/search"
  queryString          = "?jql=project%3Dtheproject%2Border%2Bby%2Bcreated"
  method               = "GET"
  proxy                = Nothing
  rawBody              = False
  redirectCount        = 10
  responseTimeout      = ResponseTimeoutDefault
  requestVersion       = HTTP/1.1
}

But the request fails with this response:

- 'Error in the JQL Query: The character ''+'' is a reserved JQL character. You must
  enclose it in a string or use the escape ''\u002b'' instead. (line 1, character
  21)'

So it seems like Jira didn't like Haskell's %2B. Do you have any suggestions on what I can do to fix this, or any resources that might be helpful? The same request sans the +order+by+created part is successful.

The code (patched together from these examples):

{-# LANGUAGE OverloadedStrings #-}
import           Data.Aeson
import qualified Data.ByteString.Char8 as S8
import qualified Data.Yaml             as Yaml
import           Network.HTTP.Simple
import           System.Environment    (getArgs)

-- auth' is echo -e "username:passwd" | base64
foo urlBase proj' auth' = do
    let proj = S8.pack (proj' ++ "+order+by+created")
        auth = S8.pack auth'
    request'' <- parseRequest urlBase
    let request'
            = setRequestMethod "GET"
            $ setRequestPath "/jira/rest/api/2/search"
            $ setRequestHeader "Content-Type" ["application/json"]
            $ request''
        request
            = setRequestQueryString [("jql", Just (S8.append "project=" proj))]
            $ setRequestHeader "Authorization" [S8.append "Basic " auth]
            $ request'
    return request

main :: IO ()
main = do
    args <- getArgs
    case args of
      (urlBase:proj:auth:_) -> do
          request <- foo urlBase proj auth
          putStrLn $ show request
          response <- httpJSON request
          S8.putStrLn $ Yaml.encode (getResponseBody response :: Value) -- apparently this is required
          putStrLn ""

      _ -> putStrLn "usage..."

(If you know a simpler way to do the above then I'd take such suggestions as well, I'm just trying to do something analogous to this Python:

import requests
import sys
if len(sys.argv) >= 4:
    urlBase = sys.argv[1]
    proj = sys.argv[2]
    auth = sys.argv[3]
    urlBase += "/jira/rest/api/2/search?jql=project="
    proj += "+order+by+created"
    h = {}
    h["content-type"] = "application/json"
    h["authorization"] = "Basic " + auth
    r = requests.get(urlBase + proj, headers=h)
    print(r.json())

)


Solution

  • project+order+by+created is the URL-encoded string for the actual request project order by created (with spaces instead of +). The function setRequestQueryString expects a raw request (with spaces, not URL-encoded), and URL-encodes it.

    The Python script you give for comparison essentially does the URL-encoding by hand.

    So the fix is to put the raw request in proj:

    foo urlBase proj' auth' = do
        let proj = S8.pack (proj' ++ " order by created")  -- spaces instead of +
        ...