diff --git a/Network/Wai/Application/Static.hs b/Network/Wai/Application/Static.hs
--- a/Network/Wai/Application/Static.hs
+++ b/Network/Wai/Application/Static.hs
@@ -6,6 +6,7 @@
       staticApp
       -- ** Settings
     , defaultWebAppSettings
+    , webAppSettingsWithLookup 
     , defaultFileServerSettings
     , StaticSettings
     , ssFolder
@@ -47,6 +48,7 @@
     , toFilePath
     , fromFilePath
     , MaxAge (..)
+    , ETagLookup
     ) where
 
 import Prelude hiding (FilePath)
@@ -229,10 +231,12 @@
 nullFilePath :: FilePath -> Bool
 nullFilePath = T.null . unFilePath
 
+{-
 stripTrailingSlash :: FilePath -> FilePath
 stripTrailingSlash fp@(FilePath t)
     | T.null t || T.last t /= '/' = fp
     | otherwise = FilePath $ T.init t
+    -}
 
 type Pieces = [FilePath]
 
@@ -304,31 +308,32 @@
     handleCache file =
       if not useHash then lastModifiedCache file
         else do
-          let mGetHash  = fileGetHash file
           let etagParam = lookup "etag" queryString
 
-          case (etagParam, mGetHash) of
-            (Just mEtag, Just getHash) -> do
-                hash <- getHash
-                if isJust mEtag && hash == fromJust mEtag
-                  then return $ FileResponse file $ ("ETag", hash):cacheControl
-                  else return $ Redirect pieces (Just hash)
-            -- a file used to have an etag parameter, but no longer does
-            (Just _, Nothing) -> return $ Redirect pieces Nothing
+          case etagParam of
+            Nothing -> do -- no query parameter. Set appropriate ETag headers
+                mHash <- fileGetHash file
+                case mHash of
+                    Nothing -> lastModifiedCache file
+                    Just hash ->
+                        case lookup "if-none-match" headers of
+                            Just lastHash ->
+                              if hash == lastHash
+                                  then return NotModified
+                                  else return $ FileResponse file $ [("ETag", hash)]
+                            Nothing -> return $ FileResponse file $ [("ETag", hash)]
 
-            _ -> 
-                case (lookup "if-none-match" headers, mGetHash) of
-                    -- etag
-                    (mLastHash, Just getHash) -> do
-                        hash <- getHash
-                        case mLastHash of
-                          Just lastHash ->
-                            if hash == lastHash
-                                then return NotModified
-                                else return $ FileResponse file $ [("ETag", hash)]
-                          Nothing -> return $ FileResponse file $ [("ETag", hash)]
-                    (_, Nothing) -> lastModifiedCache file
+            Just mEtag -> do
+                mHash <- fileGetHash file
+                case mHash of
+                  -- a file used to have an etag parameter, but no longer does
+                  Nothing -> return $ Redirect pieces Nothing
+                  Just hash ->
+                    if isJust mEtag && hash == fromJust mEtag
+                      then return $ FileResponse file $ ("ETag", hash):cacheControl
+                      else return $ Redirect pieces (Just hash)
 
+
     lastModifiedCache file =
       case (lookup "if-modified-since" headers >>= parseHTTPDate, fileGetModified file) of
           (mLastSent, Just modified) -> do
@@ -375,12 +380,12 @@
     { fileGetSize :: Int
     , fileToResponse :: H.Status -> H.ResponseHeaders -> W.Response
     , fileName :: FilePath
-    , fileGetHash :: Maybe (IO ByteString)
+    , fileGetHash :: IO (Maybe ByteString)
     , fileGetModified :: Maybe EpochTime
     }
 
 data StaticSettings = StaticSettings
-    { ssFolder :: Pieces -> IO FileLookup
+    { ssFolder :: Pieces -> IO FileLookup -- TODO: not a folder, so rename
     , ssMkRedirect :: Pieces -> ByteString -> ByteString
     , ssGetMimeType :: File -> IO MimeType
     , ssListing :: Maybe Listing
@@ -400,9 +405,14 @@
   where
     relDir = TE.encodeUtf8 (relativeDirFromPieces pieces)
 
+webAppSettingsWithLookup :: FilePath -> ETagLookup -> StaticSettings
+webAppSettingsWithLookup dir etagLookup =
+  defaultWebAppSettings { ssFolder = webAppLookup etagLookup dir}
+
+
 defaultWebAppSettings :: StaticSettings
 defaultWebAppSettings = StaticSettings
-    { ssFolder = fileSystemLookup "static"
+    { ssFolder = webAppLookup hashFileIfExists "static"
     , ssMkRedirect  = defaultMkRedirect
     , ssGetMimeType = return . defaultMimeTypeByExt . fileName
     , ssMaxAge  = MaxAgeForever
@@ -422,7 +432,7 @@
     , ssUseHash = False
     }
 
-fileHelper :: (FilePath -> Maybe (IO ByteString)) -- ^ hash function
+fileHelper :: ETagLookup
            -> FilePath -> FilePath -> IO File
 fileHelper hashFunc fp name = do
     fs <- getFileStatus $ fromFilePath fp
@@ -434,17 +444,36 @@
         , fileGetModified = Just $ modificationTime fs
         }
 
-defaultFileSystemHash :: FilePath -> Maybe (IO ByteString)
-defaultFileSystemHash fp = Just $ do
-    -- FIXME replace lazy IO with enumerators
-    -- FIXME let's use a dictionary to cache these values?
+type ETagLookup = (FilePath -> IO (Maybe ByteString))
+
+webAppLookup :: ETagLookup -> FilePath -> Pieces -> IO FileLookup
+webAppLookup cachedLookupHash prefix pieces = do
+    file <- fileHelper cachedLookupHash fp (last pieces)
+    return $ Just $ Right $ file
+  where
+    fp = pathFromPieces prefix pieces
+
+defaultFileSystemHash :: ETagLookup
+defaultFileSystemHash fp = fmap Just $ hashFile fp
+
+-- FIXME replace lazy IO with enumerators
+-- FIXME let's use a dictionary to cache these values?
+hashFile :: FilePath -> IO ByteString
+hashFile fp = do
     l <- L.readFile $ fromFilePath fp
     return $ runHashL l
 
+hashFileIfExists :: ETagLookup
+hashFileIfExists fp = do
+    fe <- doesFileExist $ fromFilePath fp
+    if fe
+      then return Nothing
+      else defaultFileSystemHash fp
+
 fileSystemLookup :: FilePath -> Pieces -> IO FileLookup
 fileSystemLookup = fileSystemLookupHash defaultFileSystemHash
 
-fileSystemLookupHash :: (FilePath -> Maybe (IO ByteString)) -- ^ hash function
+fileSystemLookupHash :: ETagLookup
                      -> FilePath -> Pieces -> IO FileLookup
 fileSystemLookupHash hashFunc prefix pieces = do
     let fp = pathFromPieces prefix pieces
@@ -494,7 +523,7 @@
     { fileGetSize = S8.length bs
     , fileToResponse = \s h -> W.ResponseBuilder s h $ fromByteString bs
     , fileName = name
-    , fileGetHash = Just $ return $ runHash bs
+    , fileGetHash = return $ Just $ runHash bs
     , fileGetModified = Nothing
     }
 
@@ -525,7 +554,7 @@
     { fileGetSize = S8.length bs
     , fileToResponse = \s h -> W.ResponseBuilder s h $ fromByteString bs
     , fileName = name
-    , fileGetHash = Just $ return $ runHash bs
+    , fileGetHash = return $ Just $ runHash bs
     , fileGetModified = Nothing
     }
 
diff --git a/wai-app-static.cabal b/wai-app-static.cabal
--- a/wai-app-static.cabal
+++ b/wai-app-static.cabal
@@ -1,5 +1,5 @@
 name:            wai-app-static
-version:         0.3.1.1
+version:         0.3.3
 license:         BSD3
 license-file:    LICENSE
 author:          Michael Snoyman <michael@snoyman.com>
@@ -48,10 +48,10 @@
     type: exitcode-stdio-1.0
 
     build-depends:   base                      >= 4        && < 5
-                   , hspec >= 0.6
+                   , hspec >= 0.6.1 && < 0.7
                    , HUnit
                    , unix-compat               >= 0.2      && < 0.3
-                   , time                      >= 1.1.4    && < 1.3
+                   , time                      >= 1.1.4    && < 1.4
                    , old-locale                >= 1.0.0.2  && < 1.1
                    , http-date
                    , Cabal
