1

I'm querying a remote API and am running into an issue where an exception is being thrown when I would not have expected it. I have inspected the types and it does not seem that there should be a problem binding the value I need to a variable.

I've aliased Network.HTTP.Client to HTTPClient, and import qualified Data.ByteString.Lazy as LBS to LBS.

Here's what my GHCi session looks like:

λ> manager <- HTTPClient.newManager HTTPClient.defaultManagerSettings
λ> request <- HTTPClient.parseUrl postsURL
λ> :t HTTPClient.httpLbs request manager
HTTPClient.httpLbs request manager
  :: IO (HTTPClient.Response LBS.ByteString)
λ> response <- HTTPClient.httpLbs request manager
*** Exception: StatusCodeException (Status {statusCode = 403, statusMessage = "Forbidden"}) [("Date","Thu, 21 Aug 2014 20:25:15 GMT"),("Server","Apache/2.2.22 (Ubuntu)"),("Vary","Accept-Encoding"),("Content-Encoding","gzip"),("Content-Length","239"),("Content-Type","text/html; charset=iso-8859-1"),("X-Response-Body-Start","<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>403 Forbidden</title>\n</head><body>\n<h1>Forbidden</h1>\n<p>You don't have permission to access /posts/all\non this server.</p>\n<hr>\n<address>Apache/2.2.22 (Ubuntu) Server at api.pinboard.in Port 80</address>\n</body></html>\n"),("X-Request-URL","GET http://api.pinboard.in:80/posts/all?format=json&auth_token=username:XXXXX")] (CJ {expose = []})

And then, of course:

λ> response

<interactive>:14:1: Not in scope: `response'

I would've thought that I could bind the result of httpLbs to response since it's wrapped in the IO monad, but it seems that somehow the exception that is raised prevents this from happening. I thought types were supposed to protect against scenarios like this, but maybe I am doing something wrong. Any help would be much appreciated.

Dan Loewenherz
  • 10,879
  • 7
  • 50
  • 81
  • 1
    It appears that your error is with the HTTP request, not the Haskell code. Do you need to authenticate before accessing the URL? – Code-Apprentice Aug 21 '14 at 20:34
  • Yep, that's on purpose. The authentication token provided is invalid, but I want to be able to read the data from the request to provide a message explaining what needs to be done to correct the error. – Dan Loewenherz Aug 21 '14 at 20:37
  • I suggest you read http://www.haskell.org/haskellwiki/Exception to learn about exception handling in Haskell. – Code-Apprentice Aug 21 '14 at 20:43
  • This should help you: http://stackoverflow.com/questions/9028636/how-can-i-catch-a-404-status-exception-thrown-by-simplehttp-of-http-conduit – Sibi Aug 21 '14 at 20:47
  • @Code-Apprentice I did--still not clicking. None of the types I'm interacting with have any sort of visible exception monad wrapper, as the wiki article describes. I.e., `IO (HTTPClient.Response LBS.ByteString)` appears to just be wrapped in IO. Where is it clear that I need to handle an exception here? – Dan Loewenherz Aug 21 '14 at 20:50
  • (besides of course running the program, which seems to counter the benefit of being able to use types to catch run-time errors) – Dan Loewenherz Aug 21 '14 at 20:51
  • I'm a Haskell noob and don't know the details that are involved here. Looks like @Sibi gave a link that should be helpful. – Code-Apprentice Aug 21 '14 at 20:58
  • @Code-Apprentice Thanks, checking that out too. Trying to piece this together. Appreciate the help. – Dan Loewenherz Aug 21 '14 at 20:58

1 Answers1

4

As a starting point for you, let me give an working example for you:

import           Network.HTTP.Conduit
import qualified Control.Exception as E
import           Network.HTTP.Types.Status
import           Data.ByteString.Lazy.Char8 (pack)

main :: IO ()
main = do
  let url = "http://www.google.com/does-not-exist.txt"
  res <- (simpleHttp url) `E.catch`
        (\(StatusCodeException s _ _) -> (return . pack . show . statusCode $ s))
  print res

In ghci:

λ> main
"404"

In the above example, I'm returing the status code 404 from the exception. You can follow the same method to do your custom logic processing for your Exception.

UPDATE: Method for working with httpLbs:

As suggested in the forum, you can wrap Maybe on top of Response and perform the exception handling:

import Network.HTTP.Client
import qualified Control.Exception as E
import Network.HTTP.Types.Status (statusCode)

main = do
  manager <- newManager defaultManagerSettings
  request <- parseUrl "http://www.google.com/does-not-exist.txt"
  resp <- (fmap Just $ httpLbs request manager) `E.catch` handleMyException
  print resp

handleMyException :: HttpException -> IO (Maybe a)
handleMyException (StatusCodeException s _ _) = if statusCode s == 404
                                                then putStrLn ((show . statusCode $ s) ++ " I'm sorry!") >> return Nothing
                                                else return Nothing

Sample demo:

λ> main
404 I'm sorry!
Nothing

I thought types were supposed to protect against scenarios like this, but maybe I am doing something wrong.

Types cannot protect you when you communicate with the real world. Doing IO activities like reading a file, connecting to a socket etc. depends upon lot of external factors which isn't under the control of Haskell (or any PL for that matter). So when things like that fail, Haskell cannot do anything. But the good thing is that Haskell makes it explicit that those are IO actions indicating that you are out of the pure domain.

Sibi
  • 47,472
  • 16
  • 95
  • 163
  • Thanks! simpleHTTP returns a wrapped ByteString, but `httpLbs` returns a ByteString wrapped with a `Response` wrapped in `IO`. I'm at a loss on how to make it work with the code in my question. – Dan Loewenherz Aug 21 '14 at 22:56
  • 1
    @Dan, I will look on what `httpLbs` returns once I reach home. If `Response` is a monad, then you can use two `return` on top of the `ByteString` to wrap it with `Response` and `IO` respectively. – Sibi Aug 22 '14 at 10:40
  • @Dan Ok, this seems to be a problem. Even their haddock code doesn't compile. I have raised this query in their [forum](https://groups.google.com/forum/#!topic/yesodweb/MhF_09k4OGk). Will update here in case they provide any workarounds. (Although I cannot come up with any workaround, there is no method to create Empty `Response` which can be wrapped in `IO` monad.) – Sibi Aug 22 '14 at 22:44
  • I just added a bounty to the question--looking for a way to handle exceptions in the code in my answer. If you can figure out a way, it's all yours. Thanks! – Dan Loewenherz Aug 23 '14 at 21:08
  • @Dan Have updated the method to include the method suggested by the forum. – Sibi Aug 23 '14 at 21:44
  • Also sent a [pull request](https://github.com/snoyberg/http-client/pull/67/files) fixing the documentation. – Sibi Aug 23 '14 at 22:04