I am not familiar at all about async. I am trying to reuse the following code:
import asynchttpserver, asyncdispatch
proc main {.async.} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT",
"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World", headers.newHttpHeaders())
server.listen Port(8080)
while true:
if server.shouldAcceptRequest():
asyncCheck server.acceptRequest(cb)
else:
poll()
asyncCheck main()
runForever()
What I want is to create a server in the middle of the code and wait for request and stop it once I received the call. So it feels like I need the synced version (I not that familiar with sync/async stuff; I mostly do synced coding).
How can I convert the code above for that purpose?
Not sure if this is what you want but check out the other thread going on right now: https://forum.nim-lang.org/t/7621
In particular @Araq posted a nice example of an http server that just uses a threadpool and no async code: https://play.nim-lang.org/#ix=2SM4
Let me rephrase what I am trying to do:
import asynchttpserver, asyncdispatch#, cgi
var server = newAsyncHttpServer()
echo "1"
proc cb(req: Request) {.async.} =
echo req.url.query
echo "3"
await req.respond(Http200, "Hello World", newHttpHeaders())
echo "2"
waitFor server.serve(Port(8080), cb)
echo "4"
The code above shows:
$ ./server
1
2
If I visit: http://localhost:8080/?key=value, I get:
$ ./server
1
2
key=value
3
3
> I don't know why I get twice 3 and only one key=value. I would expect only one 3
I woud like to stop the server at that point in order to get 4. I have tried:
import asynchttpserver, asyncdispatch#, cgi
var server = newAsyncHttpServer()
echo "1"
proc cb(req: Request) {.async.} =
echo req.url.query
echo "3"
await req.respond(Http200, "Hello World", newHttpHeaders())
server.close() # <----------------- NEW LINE
echo "2"
waitFor server.serve(Port(8080), cb)
echo "4"
but I get the error:
/home/jose/src/nimlang/google/server.nim(14, 15) Error: type mismatch: got <AsyncHttpServer, Port, proc (req: Request): Future[system.void]{.locks: <unknown>.}>
but expected one of:
proc serve(server: AsyncHttpServer; port: Port;
callback: proc (request: Request): Future[void] {.closure, gcsafe.};
address = ""; assumedDescriptorsPerRequest = 5): owned(Future[void])
first type mismatch at position: 3
required type for callback: proc (request: Request): Future[system.void]{.closure, gcsafe.}
but expression 'cb' is of type: proc (req: Request): Future[system.void]{.locks: <unknown>.}
This expression is not GC-safe. Annotate the proc with {.gcsafe.} to get extended error information.
expression: serve(server, Port(8080), cb)
But I think this shows better what I intend to do.
I think you get the 3 several times, when you test this in a browser, correct? This is because browsers do several requests implicitly, in your case it could be that the browser try to fetch the site's favicon.
This should not happen when you test with curl or "by hand" with netcat.
For the gc error, I'm not sure but I can imagine that you use the global server variable, Maybe try to also pass it to your http callback as a parameter.
Yeah, as @enthus1ast said. You're getting multiple requests from your browser, use echo req.url to make it clear. Best to test with curl to understand what's going on.
The error you're getting happens because of Nim's gc safety mechanism. You cannot access global variables in a gcsafe callback (which is what asynchttpserver expects). This is because of how threading in Nim works right now: each thread gets its own heap, so your global variable cannot be accessed from threads other than the main one. You can get rid of this error with --threadAnalysis:off, but this is a workaround.
Can you explain at a higher level what you are trying to accomplish? That way I can give more advice.
First thanks for your advices. You were right that I was trying to do so from the browser. It is ok with curl.
What I am trying to do is to perfom oauth2 authenticaton with google. So basically I open a browser where I give permission to the app, and that makes a call to something that looks like:
http://localhost:8080/?state=the_code&scope=https://www.googleapis.com/auth/drive.metadata.readonly
This is why I only need to read one request.
Basically, I am trying to make these steps more user friendly.
oh, am I understanding correctly that you're spinning up an HTTP server to authenticate your users in a desktop app and that is the reason you want to close the server as soon as the auth completes?
If so I would say that you can use global vars in your code, but I would recommend a different way: encapsulating your app data in a single State or (Game or AppData or whatever you wanna call it) type. That way you can pass it around and it can contain your http server, which you can then close without worrying about global var problems. This is how I usually structure my apps and it works well.
I was trying to encapsulate everything in a proc rather than state , without success.
In fact, I tried the code in this example and I get a similar error:
/home/jose/src/nimlang/google/server2.nim(3, 13) template/generic instantiation of `async` from here
/home/jose/src/nimlang/google/server2.nim(13, 24) Error: type mismatch: got <AsyncHttpServer, proc (req: Request): Future[system.void]{.gcsafe, locks: <unknown>.}>
but expected one of:
proc acceptRequest(server: AsyncHttpServer; port: Port; callback: proc (
request: Request): Future[void] {.closure, gcsafe.}): owned(Future[void])
first type mismatch at position: 2
required type for port: Port
but expression 'cb' is of type: proc (req: Request): Future[system.void]{.gcsafe, locks: <unknown>.}
expression: acceptRequest(server, cb)
I am compiling as usual (in case I am missing something):
nim c -r server2
Regarding the example, it seems that I hit the issues already reported here and here -associated to this bug-.
I updated to Nim1.4.4 and use the example from devel.
Nonetheless, I think that the devel example is wrong. I think it should be:
import std/[asyncdispatch,asynchttpserver]
instead of:
import std/asyncdispatch
Still playing with this (trying to actually understand what is going on).
I have the following code:
import std/[asyncdispatch, asynchttpserver]#, os
proc main {.async.} =
const port = 8080
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World", headers.newHttpHeaders())
echo "test this with: curl localhost:" & $port & "/"
server.listen(Port(port))
await server.acceptRequest(cb)
waitFor main()
when I execute it, I get the following:
$ curl http://localhost:8080
curl: (56) Recv failure: Conexión reinicializada por la máquina remota
Meaning: Connection restarted by the remote machine.
Why I am not getting Hello world in this case?
It works with:
import std/[asyncdispatch, asynchttpserver]#, os
proc main {.async.} =
const port = 8080
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
echo (req.reqMethod, req.url, req.headers)
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World", headers.newHttpHeaders())
echo "test this with: curl localhost:" & $port & "/"
server.listen(Port(port))
await server.acceptRequest(cb)
await sleepAsync(2000) # <--------------- NEW LINE
waitFor main()
I have tried also, which doesn't work either:
waitFor server.acceptRequest(cb)
#await sleepAsync(2000) # <--------------- NEW LINE
It feels like if there were some sort of race condition, but shouldn't waitFor wait until it is done?A better solution seems to be:
await server.acceptRequest(cb)
drain()
but I still don't get how waitFor works. It doesn't seem to block the required ammount of time.It seems like you missing the following line:
if server.shouldAcceptRequest():
Not sure if that'd make a difference, but please do tell.For some reason, when I tried to use just serve in my code I had issues. I just want to create an http server that receives one controlled request and that's it.
The only solution that I have managed to make work in a controlled manner is the one using:
await server.acceptRequest(cb)
drain()
For an old noob like me, I struggle to understand the behaviour of asynchttpserver. For example, the behaviors that I mention before about waitFor.
So right now, I am sticking with what is working for me.
This is the code that I am using to play with, which is rubbish (I know), but does what I expect.
I was trying to use googleapi from @treeform. But it uses service accounts, which doesn't work for my use case.
import oauth2
import strutils, strformat
import std/httpclient
import json
import os, streams, uri
import browsers
import asynchttpserver, asyncdispatch, cgi
const
SCOPES = @["https://www.googleapis.com/auth/spreadsheets"]#"https://www.googleapis.com/auth/drive"] #"https://www.googleapis.com/auth/gmail.readonly"]
CREDENTIALSFILE = "client_secret.json"
let
port:int = 8080 # Redirect port (it should be taken form the credentials)
SPREADSHEETID: string = "1DDMKSydnQ1sn1Y1iG7nslOIi2sIOWyZeauaTrB7LM9k"
type
Credentials* = object
authorizeUrl*:string
accessTokenUrl*:string
redirectUri*:string
clientId*:string
clientSecret*:string
Token* = object
state*, code*, scope*:string
proc readCredentials(fileName:string = "client_secret.json"):Credentials =
var cred:Credentials
let credentialsNode = parseFile(fileName)
cred.authorizeUrl = credentialsNode["installed"]["auth_uri"].getStr
cred.accessTokenUrl = credentialsNode["installed"]["token_uri"].getStr
cred.redirectUri = credentialsNode["installed"]["redirect_uris"][1].getStr
cred.clientId = credentialsNode["installed"]["client_id"].getStr
cred.clientSecret = credentialsNode["installed"]["client_secret"].getStr
return cred
proc openAuthorizationBrowser(cred:Credentials, state: string, scopes:seq[string]) =
let grantUrl = getAuthorizationCodeGrantUrl( cred.authorizeUrl, cred.clientId,
cred.redirectUri, state, SCOPES )
openDefaultBrowser(grantUrl)
# Receives redirect url. You can also handle directly from server that was launched.
#------------
# Creates a local server and waits for connection in order to receive the token
proc get_token():Future[Token] =
var server = newAsyncHttpServer()
var state, code, scope: string
proc cb(req: Request) {.async.} =
for k, v in req.url.query.decodeData:
case k:
of "state": state = v
of "code" : code = v
of "scope": scope = v
else: echo k, " --> ", v
#if state != "" and code != "" and scope != "":
await req.respond(Http200, "Close this tab", newHttpHeaders())
#else:
# await req.respond(Http200, "Something went wrong", newHttpHeaders())
server.listen port.Port
waitFor server.acceptRequest(cb)
drain()
var retFuture = newFuture[Token]("get_token")
retFuture.complete(Token(state:state, code:code, scope: scope))
return retFuture
#------
proc getToken(cred:Credentials, code:string):JsonNode =
let
client = newHttpClient()
response = client.getAuthorizationCodeAccessToken(
cred.accessTokenUrl,
code,
cred.clientId,
cred.clientSecret,
cred.redirectUri
)
let resultStr = response.bodyStream.readAll()
return parseJson(resultStr)
#-------------
# Guardamos el token obtenido
var r:JsonNode
if fileExists("token.json"):
r = parseFile("token.json")
else:
let
cred = readCredentials(CREDENTIALSFILE)
state = generateState()
openAuthorizationBrowser(cred, state, SCOPES)
let data = waitFor get_token()
#echo repr data
echo "State: ", data.state
echo "Code: ", data.code
echo "Scope: ", data.scope
assert state == data.state
r = getToken(cred, data.code)
writeFile("token.json", $r)
#let resp = await client.get(url)
#]#
#[
let q = "archlinux"
let myurl = &"https://www.googleapis.com/drive/v3/files" #?q={encodeUrl(q)}"
echo myurl
let resp2 = client2.get(myurl)
let resultStr2 = resp2.bodyStream.readAll()
echo pretty parseJson(resultStr2)
]#
# GOOGLE SHEETS
#[
proc setValues*(
conn: Connection,
spreadsheetId,
valueRange: string,
data: JsonNode
): Future[JsonNode] {.async.} =
return await conn.put(
&"{sheetsRoot}/spreadsheets/{spreadsheetId}/values/{valueRange}?valueInputOption=USER_ENTERED",
data
)
]#
#=======
const sheetsRoot = "https://sheets.googleapis.com/v4"
type
Connection = object
client:HttpClient
proc getConnection( accessToken:string ):Connection =
var conn:Connection
conn.client = newHttpClient()
#var client = newHttpClient()
conn.client.headers = newHttpHeaders({
"Authorization": "Bearer " & accessToken,
"Content-Type": "application/json"
})
return conn
let conn = getConnection($r["access_token"])
proc getSpreadsheet*(conn:Connection,spreadsheetId: string ): JsonNode = # Future[JsonNode] {.async.}
let myurl = &"{sheetsRoot}/spreadsheets/{spreadsheetId}" #?q={encodeUrl(q)}"
let response = conn.client.get(myurl)
return parseJson(response.bodyStream.readAll)
proc getValues*(conn: Connection, spreadsheetId, valueRange: string): JsonNode = #Future[JsonNode] {.async.} =
let response = conn.client.get(&"{sheetsRoot}/spreadsheets/{spreadsheetId}/values/{valueRange}")
return parseJson(response.bodyStream.readAll)
let sheet = conn.getSpreadsheet(SPREADSHEETID)
echo pretty sheet
let values = conn.getValues(SPREADSHEETID, "Hoja 1!A1:B4")
echo pretty values
#echo parseJson(resp.bodyStream.readAll)
#var spreadsheetJson = await conn.getSpreadsheet(spreadsheetId)
#echo spreadsheetJson
#
#
#echo myurl
#