I am developing a web application in Haskell using Servant. I succeeded to load files on server and move them to my SFTP server. Now I want the user to be able to get the file loaded on SFTP (I need to serve to them static files with servant). To manage CORS problems I used WAI middleware library. This is my code where I introduce middleware to set CORS headers.
newtype Welcome
= Welcome {
greeting :: String
}
deriving (Generic, Eq, Show)
instance ToJSON Welcome
instance FromJSON Welcome
welcomeMesg :: Welcome
welcomeMesg = Welcome "Mbote Ba'a Mpagi!"
type API =
-- | test for home page
"home" -- 1
:> Get '[JSON] Welcome
:<|> "tenants"
:> Capture "tenantid" Text
:> "informations"
:> MultipartForm Tmp (MultipartData Tmp)
:> Post '[JSON] (GenericRestResponse InformationCreated ConstructionError)
:<|> "tenants" -- 3
:> Capture "tenantid" Text
:> "informations"
:> Capture "informationid" Text
:> "documents"
:> Raw
addOriginsAllowed :: Response -> Response
addOriginsAllowed =
let final = (:) ("Access-Control-Allow-Origin", "*") .
(:) ("Access-Control-Allow-Methods", "GET, POST, PUT, OPTION") .
(:) ("Access-Control-Allow-Headers", "Content-Type, Authorization")
in mapResponseHeaders final
addAllOriginsMiddleware :: Application -> Application
addAllOriginsMiddleware baseApp req responseFunc =
let newResponseFunc :: Response -> IO ResponseReceived
newResponseFunc = responseFunc . addOriginsAllowed
in baseApp req newResponseFunc
startApp :: IO ()
startApp = run 9080 $ addAllOriginsMiddleware app
proxy :: Proxy API
proxy = Proxy
optionMidlleware :: Middleware
optionMidlleware = provideOptions proxy
app :: Application
app = optionMidlleware $ serve proxy routes
routes :: Server API
routes =
return
welcomeMesg -- 1
:<|> handleCreateInformation --2
:<|> handlerGetInformationDocuments -- 3
All is working well if I don't make an endpoint where I ask for Raw to serve static files. The last endpoint generate this error:
• No instance for (servant-foreign-0.16:Servant.Foreign.Internal.GenerateList
Servant.API.ContentTypes.NoContent
(http-types-0.12.4:Network.HTTP.Types.Method.Method
-> servant-foreign-0.16:Servant.Foreign.Internal.Req
Servant.API.ContentTypes.NoContent))
arising from a use of ‘provideOptions’
(maybe you haven't applied a function to enough arguments?)
• In the expression: provideOptions proxy
In an equation for ‘optionMidlleware’:
optionMidlleware = provideOptions proxytypecheck(-Wdeferred-type-errors)
provideOptions :: forall api.
(GenerateList NoContent (Foreign NoContent api),
HasForeign NoTypes NoContent api) =>
Proxy api -> Middleware
Defined in ‘Network.Wai.Middleware.Servant.Options’ (servant-options-0.1.0.0)
_ :: Proxy API -> Middleware
And this error occurred at the point where I call provideOptions (function of WAI library for middleware)
I am blocked. What can I do to overcome this error?
I try to return ByteString but i is not an instance of ToJson and FromJson
This error is occurring because a
Rawendpoint is just an "escape hatch" to non-Servant request/response processing, and soprovideOptions, which is Servant-specific middleware, has no way of correctly handling aRawendpoint in an API.In more detail,
provideOptionsuses endpoint-specificHasForeigninstances to query the API for supported methods in order to construct a correctAllowheader in responding to anOPTIONSrequest. It can do this for all the Servant endpoints exceptRaw, which doesn't have aHasForiegninstance.It might be possible to get something to work in your specific case, but my guess is that you don't need a
Rawendpoint here at all. The examples in the Servant cookbook use aRawendpoint to serve static files, but only because they want to use an essentially non-Servant handler function (serveDirectoryWebApp) to serve a whole directory. (Yes,serveDirectoryWebAppis defined in theservant-serverpackage, but it's really a non-Servant handler. It doesn't really use the Servant API or any Servant handler facilities. It's just a wrapper aroundserveDirectoryWithfromNetwork.Wai.Application.Static.)In your case, it sounds like you're just trying to write a plain Servant handler
handlerGetInformationDocumentsthat is accessing the file directly and collecting its contents in aByteString, and you want to provide thatByteStringas the content returned in response to the client'sGETrequest. If so, instead of aRawendpoint, you just want aGetendpoint that returns an octet stream, something like:and the handler can just return the
ByteString:Above, I used a lazy
ByteString, which is probably best if the files are somewhat large, though you want to take care not to modify a file in place once you've started serving it. StrictByteStrings would work fine, too, especially if the files are small.Update:
Unfortunately, as per your follow up comment, this still doesn't work, and you get an error about
JSON expected. The problem is that, as mentioned above,provideOptionsuses theservant-foreignpackage, and this package only supportsJSONendpoints. This is (barely) documented in a comment in the documentation:Unfortunately, I think the most reasonable fix is to toss out
servant-optionsand write your ownprovideOptionsthat doesn't depend onservant-foreign. This can be done with aHasMethodsclass with instances for the API types that enumerate all the methods for a given path:You can test this out on your API by running things like:
Then, you can write a
provideOptionsmiddleware function that constructs the correctAllowheader based on thegetMethodsresult for the current request path:Anyway, here's a complete runnable example based on your original code showing this new
provideOptionsimplementation at work. It hosts a web form at:that lets you POST a file to:
and retrieve files by numeric ID at:
The middleware appears work correctly. For example:
The full example: