4
\$\begingroup\$

I'm currently taking a look at Snap with Haskell. I'm pretty new to Haskell and functional programming in general and am working out the best way to handle errors. Currently I have something like this

createReport :: Handler b Api ()
createReport = do
report <- readRequestBody 65536
modifyResponse $ setHeader "Content-Type" "application/json"
case (decode report :: Maybe Report) of
  Just x -> do
    execute "INSERT INTO \"Reports\" (name) VALUES (?)" [name x]
    -- Do some other stuff and return a response
  Nothing -> badRequest

badRequest :: Handler b Api()
badRequest = do
 modifyResponse . setResponseCode $ 400
 writeLBS "Bad Request"

I'm really wondering if this is the best way to go about errors or is there a better way to handle errors, specifically within Snap. Also any other comments you may have would be great.

Full code here https://github.com/elliotstokes/report-generator

\$\endgroup\$
  • \$\begingroup\$ Please note that on Code Review, we prefer you to not strip away code for the purpose of posting it here. Unlike Stack Overflow, we prefer to look at real code instead of example code. (Whether or not this actually is example code is something I leave to people who are more familiar with Haskell) \$\endgroup\$ – Simon Forsberg Jul 14 '15 at 9:42
  • 1
    \$\begingroup\$ The code is in Github your more than welcome to look at it there. I ommitted it to show the main issue I was facing. Code here: github.com/elliotstokes/report-generator \$\endgroup\$ – Elliot Stokes Jul 14 '15 at 12:22
2
\$\begingroup\$

What's "bad" about the request?

Giving a generic failure response isn't terribly useful to the poor soul who has to deal with handling the errors when interfacing with your API. Why did it fail? You're only catching the case where the response either is or isn't valid JSON, but what about other kinds of failure?

One thing you might want to look at is Digestive Functors, if you haven't already. You can use it to do some of your error handling (eg. the value for FieldB is out of the specified range, etc.). There's a Digestive Functors Aeson library that will let you send your JSON result through the form. I don't do much with passing around JSON data myself, so this might be a bit more than you need.

The database can fail, too

You've only handled the case where decoding your JSON request can fail, not the database portion. When it comes to the database, there are 4 basic outcomes to a query:

  • Success: Results
  • Success: No results
  • Failure: Constraint Violations (errors caused by the user)
  • Failure: SQL errors (errors caused by the developer)

So for queries that can fail (any time you run an insert/update/delete query), you'll want to make sure you're catching the Constraint Violations. PostgreSQL Simple has a convenient function in Database.PostgreSQL.Simple.Errors (catchViolation) specifically for that (though if you're using the PostgreSQL Simple snaplet rather than using PostgreSQL Simple directly, you'll have to roll your own because the type signatures are incompatible).

Snap Extras might be right for you

There's another package called snap-extras that contains useful functions like reading the response as JSON or responding with JSON. I know some people aren't too fond of it since it has a lot of dependencies, but I tend to use some of the other modules in it, so it's convenient for me.

If not, you can easily copy/paste just the parts you need into your own project.

\$\endgroup\$
  • \$\begingroup\$ Again. Great comments. I hadn't looked at digestive functors but they sound like exactly what I am after. I will also look at Snap extras \$\endgroup\$ – Elliot Stokes Jul 14 '15 at 20:24
1
\$\begingroup\$

Snap catches all exceptions thrown within handlers and converts them into responses with HTTP 500 code. So error handling in your code is pretty fine (if you are ok with converting "postgres is down" or "invalid query" into HTTP 500 code).

Side note 1: You are setting Content-Type at the top of the handler. It is easy to forget to add/remove this code if you are modifying some part of handler that is actually writes response data. It may be more convenient to define helper function:

writeJSON :: Aeson.ToJSON v => v -> Handler a b ()
writeJSON v = do
   modifyResponse $ setContentType "application/json"
   writeLBS $ Aeson.encode v

Side note 2: Some libraries export functions with quite generic names (like execute or decode), it is easier to read code when you have some explicit pointer about were function comes from.

You can import library with alias:

import Snap.Snaplet.PostgresqlSimple as PG
... 
PG.execute ...

Or use explicit import list:

import Data.Aeson (decode)

Haskell wiki has a bit of text on this topic.

\$\endgroup\$
  • 1
    \$\begingroup\$ Yes, but a 500 response has a completely different meaning from a 400 response. Letting Snap catch it is less than ideal. \$\endgroup\$ – cimmanon Jul 14 '15 at 16:14
  • \$\begingroup\$ I agree. I think that I would prefer to stick with my way of catching the errors and keep the http code than have to show a 500. \$\endgroup\$ – Elliot Stokes Jul 14 '15 at 16:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.