How do I store data in Suave userstate? - f#

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")

Related

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
})

Got the an intermediate value in the last chained Option.bind?

I have the following code
let getHtml location =
let request (url:string) =
let response = httpRequest (getFullUri url)
response.Headers.TryFind "Location"
request location
|> Option.bind (fun x -> request x)
|> Option.bind (fun x -> request x) // need the return of httpRequest inside request
I want the code return the last call of httpRequest. Not the return of request.
Update: tried the following code. Error on the last snd. I think I can use a mutable variable to implement it. But is it F# idiomatic?
let getHtml location =
let request (url:string) =
let response = httpRequest (getFullUri url)
match response.Headers.TryFind "Location" with
| Some location -> Some location, response
| None -> None, response
request location |> fst
|> Option.bind (fun x -> request x |> fst)
|> Option.bind (fun x -> request x |> snd) // Error on snd
Use mutable variable?
let getHtml location =
let mutable resp : FSharp.Data.HttpResponse = ???
let request (url:string) =
let response = httpRequest (getFullUri url)
resp <- response
response.Headers.TryFind "Location"
request location
|> Option.bind (fun x -> request x)
|> Option.bind (fun x -> request x)
if not (resp = null) then Some resp else None
I think want you want to do is actually make getHtml recursive, so that when an HTTP request returns a 201 or a 300-level response code, you follow the Location header to the redirected page and return the correct HTML. You could do that with a simple pattern match on the response.StatusCode and the location header, as follows:
open FSharp.Data
// stub
let getFullUri (url: string) =
sprintf "%A" <| System.UriBuilder(url)
// stub
let httpRequest = Http.Request
// fetches the requested URL, following redirects as necessary
let rec getHtml location =
let response = httpRequest (getFullUri location)
match response.StatusCode, response.Headers |> Map.tryFind "Location" with
| (status, Some redirectUrl) when status = 201 || (status >= 300 && status < 400) ->
getHtml redirectUrl
| _ ->
response
Is that what you were going for? I tested it with the following URL that returns a 302, and it got the HTML for the page to which it was redirected: https://jigsaw.w3.org/HTTP/300/302.html

Suave not showing static file

So I have my server set up very simply. If the path is of the form /article/something, it should serve up the static file something.html within the folder static. For some reason, the Files.file webpart is apparently returning None. I tacked on the OK "File Displayed" webpart to verify that this is the case. The OK never executes.
let app =
choose [
pathScan "/article/%s" (fun article ->
let name = sprintf "%s.html" article
Console.WriteLine name
Files.file name >=> OK "File Displayed")
]
let config =
{ defaultConfig with homeFolder = Some (Path.GetFullPath "./static") }
[<EntryPoint>]
let main args =
startWebServer config app
0
Interestingly enough, the Console.WriteLine name line executes perfectly and I see something.html in the console window when I execute this. It appears the problem is exclusively Files.file name returning None.
The file something.html definitely exists in the static folder, so that's not the problem .
Any ideas on what might be causing this?
Here are some parts to troubleshoot static file serving issues
let troubleShootExtensionPart extensionToCheck :WebPart =
fun ctx ->
match extensionToCheck with
| null | "" -> ServerErrors.INTERNAL_ERROR "Extension Error not supplied, part is not set up correctly"
| x when not <| x.StartsWith "." -> ServerErrors.INTERNAL_ERROR "Extensions start with a '.', part is not set up correctly"
| _ ->
let mtm = ctx.runtime.mimeTypesMap
match mtm extensionToCheck with
| None ->
sprintf "%s is not supported by the mime types map, compose your mime type with the `defaultMimeTypesMap`" extensionToCheck
|> RequestErrors.FORBIDDEN
| Some x ->
sprintf "%s is supported and uses '%s', compression on? : %A" extensionToCheck x.name x.compression
|> OK
|> fun wp -> wp ctx
example consumption with a wildcard so if no routes match you get some diagnostic info
#if DEBUG
pathScan "/checkExtension/%s" (fun name -> troubleShootExtensionPart name)
// catch all
(fun ctx -> sprintf "404, also homeFolder resolves to %s" (Path.GetFullPath ".") |> RequestErrors.NOT_FOUND |> fun wp -> wp ctx)
#endif

Is that possible to create a class instance with webpart GET values

I created a "Game" class and i'm trying to use values from my webpart path to create an instance of it.
My instance need a playerName so i tried to create one with the name value
let g:game.Game = new game.Game()
let php =
request (fun r ->
match r.queryParam "playerName" with
| Choice1Of2 name -> new game.Game(1,name,"hyy")//OK (sprintf "playerName: %s" name)
| Choice2Of2 msg -> BAD_REQUEST msg)
let webPart =
choose [
path "/" >=> (OK "Home")
path "/elm/api/create.php" >=> php
]
startWebServer defaultConfig webPart
but it doesn't work because this expression is supposed to be HttpContext type and not Game type.
I'd like to create an instance and call class's methods depending on my path values.
first: you cant return 2 different types from your function
let php =
request (fun r ->
match r.queryParam "playerName" with
| Choice1Of2 name -> new game.Game(1,name,"hyy")
^^^^^^^^^^^
//should probably be a OK
//OK (sprintf "playerName: %s" name)
| Choice2Of2 msg -> BAD_REQUEST msg)
Then you also should Jsonify your Game object. So probably your code should look somehow like this
| Choice1Of2 name ->
new game.Game(1,name,"hyy")
|> toJson
|> OK
please substitute toJson with a call of your chosen Json library

Aggregating info from request in Suave

I am building an authenticated web API using Suave, and I often stumble on the problem of aggregating infos throughout different functions
pathScan Navigation.playersAvailables GetGame >>= getInSession >>= (fun (gameId,playerInSession) -> //access to both gameId and player in session)
signatures :
getGame : HttpContext -> Async<HttpContext option>
getInSession : HttpContext -> Async<HttpContext option>
getGame take id from httpContext.request.querystring getInSession
take sessionId from httpContext.cookie
The only thing I found in order to do that was to store infos in the userDataDictionnary :
Writers.setUserData "player" { playerId= playersId; socialId=socialId; username = username}
And to retrieve it in the other function but it looks pretty nasty to me :
let player = x.userState.["player"] :?> PlayerSession
//do some other stuff now that we have the current player
Is there another way of doing that? I would like to have pure functions like
getGameId and get Session, etc.. and to be able to compose them as I wish to process my different routes :
pathScan Navigation.playersAvailables GetGame >>= getInSession >>= (fun (gameId,playerInSession) -> //access to both gameId and player in session)
pathScan Navigation.otherRoute GetGame >>= (fun (gameId) -> //process gameId)
pathScan Navigation.otherRoute2 getInSession >>= (fun (sessionId) -> //process sessionId to do some other stuff)
I am afraid that what I need is a day talk with some real functionnal programmer..
setUserData is a pure function - src.
Not sure if this is still current but it says pathScan and >>= cannot be nicely chained. However I think the Writers.setUserData you are using may be able to accomplish it.
Accessing an object bag to pull things out isn't lovely.
how about:
let (|ParseInt|_|) =
function
| "" | null -> None
| x ->
match Int32.TryParse x with
| true, i -> Some i
| _ -> None
let (|HasParam|_|) name (ctx:HttpContext) =
ctx.request.queryParam name
|> function
|Choice1Of2 value ->
Some value
| _ -> None
let playersAvailablePart:WebPart =
function
//access to both gameId and player in session
|HasParam "inSession" playerInSession & HasParam "gameId" gameId as ctx ->
// do your processing here, sample return:
OK "we had all the required important parts" ctx
// or an example of something more strongly typed
| HasParam "inSession" (ParseInt playerInSession) & HasParam "gameId" (ParseInt gameId) as ctx ->
// do your processing here, sample return:
OK "we had all the required important parts" ctx
| ctx -> never ctx
This doesn't exactly work if the values aren't in the queryParameters, but you can adapt it to where they are

Resources