How to handle "OPTIONS" requests in suave - f#

I'm porting a prototype app I did using elm and python flask to use elm and a suave backend. The elm app is calling an API to load info from the site and do some other things. There does not seem to be an issue with get requests, but I'm getting funny behaviour when doing the POST from elm - works for flask but the request does not seem to be accepted by suave.
Sorry for the long post, details below:
Elm code:
--POST IS WORKING TO Flask BUT NOT TO Suave
testPassword editMode token =
let
data =
Json.Encode.object
[ ( "email", Json.Encode.string editMode.email )
, ( "password", Json.Encode.string editMode.newValue )
]
body =
Json.Encode.object [ ("data", data) ]
decodeVal value =
Json.Decode.map2 RestResponse
(field "success" Json.Decode.bool)
(field "errorMessage" Json.Decode.string)
valDecoder =
Json.Decode.value
|> Json.Decode.andThen decodeVal
postTo =
String.concat [ apiUrl, token, "/api/password/test" ]
in
Json.Decode.field "data" valDecoder
|> Http.post (postTo) (jsonBody body)
|> Http.send UpdateValue
Debugging in chrome I can see for python flask the OPTIONS request goes through and the response indicates a POST is required
General
Request URL: http://localhost:5000/api/password/test
Request Method: OPTIONS
Status Code: 200 OK
Remote Address: 127.0.0.1:5000
Referrer Policy: no-referrer-when-downgrade
Response Headers
Access-Control-Allow-Headers: *
Access-Control-Allow-Origin: *
Allow: OPTIONS, POST
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 30 May 2018 09:35:08 GMT
Server: Werkzeug/0.14.1 Python/3.5.2
However, with Suave, the OPTIONS request is incomplete or interrupted:
General
Request URL: http://localhost:8080/api/password/test
Referrer Policy: no-referrer-when-downgrade
Request Headers
Provisional headers are shown
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: http://localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36
My question is what do I need to do on the suave program side to get this working?
I suspect it is either suave configuration or I need to code a WebPart to respond to the options request. Suave code below:
let setCORSHeaders =
Console.WriteLine("Enabling cross origin requests")
addHeader "Access-Control-Allow-Origin" "*"
>=> setHeader "Access-Control-Allow-Headers" "token"
>=> addHeader "Access-Control-Allow-Headers" "content-type"
>=> addHeader "Access-Control-Allow-Methods" "GET,OPTIONS,POST,PUT"
let allowCors : WebPart =
choose [
GET >=>
fun context ->
context |> (
setCORSHeaders )
]
let app =
..
statefulForSession
>=> allowCors
>=> choose
[ GET >=> choose
[ //..
]
POST >=> choose
[ //other posts working
path "/api/password/test" >=> context apiController.passwordTest
]
OPTIONS >=> choose
[ //tried this as well but don't think it's correct
path "/api/password/test" >=> context apiController.passwordTest
] ]
let suaveCfg =
{ defaultConfig with
serverKey = Convert.FromBase64String encodedkey
cookieSerialiser = new JsonNetCookieSerialiser()
}
[<EntryPoint>]
let main argv =
startWebServer suaveCfg app
0
Thanks for reading

I suspect that this is your problem:
let allowCors : WebPart =
choose [
GET >=>
fun context ->
context |> (
setCORSHeaders )
]
There's no OPTIONS case here, so when an OPTIONS request comes through your app, the choose combinator finds nothing that matches the request and so returns None, which means further parts of the handling chain are never called. And in your app, allowCors comes before the part of the chain that handles OPTIONS:
let app =
..
statefulForSession
>=> allowCors
>=> choose
[ GET >=> choose
[ //..
]
// Elided the POST part here
OPTIONS >=> choose
[ //tried this as well but don't think it's correct
path "/api/password/test" >=> context apiController.passwordTest
] ]
Put an OPTIONS section in your allowCors WebPart and your code should work, I think.
Edit: Also, this chunk of code in allowCors can be improved:
fun context ->
context |> (
setCORSHeaders )
Any time you have fun a -> a |> otherFunction, you can replace that expression with otherFunction. So your allowCors function, as written, looks like this:
let allowCors : WebPart =
choose [
GET >=>
fun context ->
context |> (
setCORSHeaders )
]
But it could look like this instead:
let allowCors : WebPart =
choose [
GET >=> setCORSHeaders
]
Much easier to read, don't you think?

Related

F#- Using HttpFs.Client and Hopac: How do I get a response code, response headers and response cookies?

I am using F# with HttpFs.Client and Hopac.
I am able to get Response body and value of each node of JSON/XML response by using code like:
[<Test>]
let ``Test a Create user API``() =
let response = Request.createUrl Post "https://reqres.in/api/users"
|> Request.setHeader (Accept "application/json")
|> Request.bodyString ReadFile
|> Request.responseAsString
|> run
printfn "Response of get is %s: " response
let info = JsonValue.Parse(response)
let ID = info?id
printfn "ID in Response is %i: " (ID.AsInteger())
But how do I get a response code, response headers, and response cookies? I need to get this inside the same method as shown above so that I can do the assertion on these items too.
I did try response.StatusCode, response.Cookies.["cookie1"] but there are no such methods comes up when I add period after response.
let response =
Request.createUrl Post "https://reqres.in/api/users"
|> Request.setHeader (ContentType (ContentType.create("application", "json")))
|> Request.bodyString token //Reading content of json body
|> HttpFs.Client.getResponse
|> run
Please read the doc https://github.com/haf/Http.fs
Point 3 shows how to access cookies and headers in the response.
response.StatusCode
response.Body // but prefer the above helper functions
response.ContentLength
response.Cookies.["cookie1"]
response.Headers.[ContentEncoding]
response.Headers.[NonStandard("X-New-Fangled-Header")]

How to make F# Giraffe default route points to /health?

I have the following Program.fs:
let webApp = choose [ setStatusCode 404 >=> text "Not Found" ]
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
let configureApp (app : IApplicationBuilder) =
app.UseHealthChecks(PathString("/health")) |> ignore
let env = app.ApplicationServices.GetService<IHostingEnvironment>()
(match env.IsDevelopment() with
| true -> app.UseDeveloperExceptionPage()
| false -> app.UseGiraffeErrorHandler errorHandler)
.UseHttpsRedirection()
.UseStaticFiles()
.UseGiraffe(webApp)
let configureServices (services : IServiceCollection) =
services.AddHealthChecks() |> ignore
services.AddCors() |> ignore
services.AddGiraffe() |> ignore
let configureLogging (builder : ILoggingBuilder) =
builder.AddFilter(fun l -> l.Equals LogLevel.Error)
.AddConsole()
.AddDebug() |> ignore
[<EntryPoint>]
let main _ =
let contentRoot = Directory.GetCurrentDirectory()
let webRoot = Path.Combine(contentRoot, "WebRoot")
WebHostBuilder()
.UseKestrel()
.UseContentRoot(contentRoot)
.UseIISIntegration()
.UseWebRoot(webRoot)
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
I would like that the default endpoint target the health check (ie. /health), how is that possible using F# Giraffe?
I followed the article given in comment: https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#redirection
and ended up on the following choose:
let webApp = choose [
GET >=>
choose [
route "/" >=> redirectTo false "/health"
]
setStatusCode 404 >=> text "Not Found" ]

Can't get WebSharper Javascript Client to run hosted on Suave

I am trying out the example snippet titled "Adding client-side functionality" from the following page :
https://developers.websharper.com/docs/v4.x/fs/overview
It looks a bit outdated and doesn't compile as is, so based on the original repository where that code was snipped from, this is what I have now.
namespace TestSuaveWs
open WebSharper
open WebSharper.Sitelets
open WebSharper.UI
open WebSharper.UI.Html
open WebSharper.UI.Client
module Server =
[<Rpc>]
let DoWork (s: string) =
async {
return System.String(List.ofSeq s |> List.rev |> Array.ofList)
}
[<JavaScript>]
module Client =
open WebSharper.JavaScript
open WebSharper.Html.Client
let Main () =
let input = Input [ Attr.Value "" ]
let output = H1 []
Div [
input
Button [ Text "Send" ]
|>! OnClick (fun _ _ ->
async {
let! data = Server.DoWork input.Value
output.Text <- data
}
|> Async.Start
)
HR []
H4 [ Class "text-muted" ] -- Text "The server responded:"
Div [ Class "jumbotron" ] -< [ output ]
]
module TheSite =
open WebSharper.UI.Server
[<Website>]
let MySite =
Application.SinglePage (fun ctx ->
Content.Page(
Body = [
h1 [] [ text "Say Hi to the server" ]
div [] [ client <# Client.Main() #> ]
]
)
)
open global.Suave
open Suave.Web
open WebSharper.Suave
let webPart = WebSharperAdapter.ToWebPart(MySite, RootDirectory="../..")
Then there's the main program.
namespace TestSuaveWs
module Main =
open System
open System.Threading
open Suave
[<EntryPoint>]
let main argv =
let cts = new CancellationTokenSource()
let conf = { defaultConfig with cancellationToken = cts.Token }
let listening, server = startWebServerAsync conf TheSite.webPart
Async.Start(server, cts.Token)
printfn "Make requests now"
Console.ReadKey true |> ignore
cts.Cancel()
0
The program runs, and I can see the text "Say Hi to the server" on localhost:8080, but there is nothing below that text. A picture on the page with the example shows what it should look like. There's supposed to be a text input field, a button, and a reply text.
When I open Developer Tools in my Chrome browser, I can see that there's a bunch of similar messages, differing only in the javascript filename, that says "Failed to load resource: the server responded with a status of 404 WebSharper.Main.min.js:1 (Not Found)"
There are no *.js files in the bin\Debug folder. I am running in VS 2019, with .NET Framework 4.7.1.
What am I missing?
I wasn't aware that this very example was available as a template named "WebSharper 4 Suave-hosted Site", which I only found after downloading and installing the WebSharper vsix. That template spun up a project doing exactly what I tried to achieve. So that's the answer. I wish this was hinted at in the documentation page.

Custom dynamic response with Suave?

I want to build a simple counter with Suave.
[<EntryPoint>]
let main argv =
let mutable counter = 0;
let app =
choose
[
GET
>=> choose
[
path "/" >=> OK "Hello, world. ";
path "/count" >=> OK (string counter)
]
POST
>=> choose
[
path "/increment"
>=> (fun context -> async {
counter <- counter + 1
return Some context
})
]
]
startWebServer defaultConfig app
0
However, with my current solution, the count at /count never updates.
I think this is because the WebPart is computed when the app is launched, instead of for each request.
What is the best way to achieve this in Suave?
You are right in the assumption that Webparts are values, so computed once. (See this).
You need to use a closure to get what you want:
path "/count" >=> (fun ctx ->
async {
let c = counter in return! OK (string c) ctx
})

How do I store data in Suave userstate?

I have a web part that handles an OAuth callback request.
After getting access tokens and the user's id from the API, I want to store it in the session state. But when reading the session on subsequent requests, I only see a value for the "Suave.Auth" key.
Here is my web part for the OAuth callback:
path "/oauth" >=> context (fun ctx ->
let req = ctx.request
match (req.queryParam "code", req.queryParam "error") with
| Choice1Of2 code, _ ->
let id = completeOAuth code
Authentication.authenticated Session false
>=> Writers.setUserData "user-id" id
>=> Redirection.redirect "/view"
| _, Choice1Of2 error -> RequestErrors.UNAUTHORIZED error)
How can I make sure the "user-id" value is in the session on other requests after this one?
Writers.setUserData stores data in a map that only exists for the duration of the request.
To store data across requests you need to use statefulForSession like this.
let app =
statefulForSession >=> context (fun x ->
match HttpContext.state x with
| None ->
// restarted server without keeping the key; set key manually?
let msg = "Server Key, Cookie Serialiser reset, or Cookie Data Corrupt, "
+ "if you refresh the browser page, you'll have gotten a new cookie."
OK msg
| Some store ->
match store.get "counter" with
| Some y ->
store.set "counter" (y + 1) >=> OK (sprintf "Hello %d time(s)" (y + 1))
| None ->
store.set "counter" 1 >=> OK "First time")

Resources