Hi!
I'm doing some work with Google Drive API. To obtain the access token, I have to receive a request, make another request, and extract data from it. So I need a proc that would start a server, receive a request, do a request, receive another request, shut down the server, and return the response. I'm using the standard asynchttpserver and httpclient modules.
The main issue is how to stop the server from inside its callback proc. This is how I do it:
proc getAccessCode(creds: JsonNode): string =
var
server = newAsyncHttpServer()
serverFut: Future[void] # explicitly declare the return future for the server
accessToken: string
proc callback(req: Request) {.async.} =
let
data = newMultipartData({...})
resp = postContent(creds["token_uri"].getStr, multipart=data)
accessToken = parseJson(resp)["access_token"].getStr
await req.respond(Http200, "")
serverFut.complete() # explicitly complete the server future declared in the closure
serverFut = server.serve(Port(8080), callback) # explicitly set the value for the server future...
waitFor serverFut # ...and wait for it
return accessToken
This method works just fine, but I don't like it for its explicitness. Also, using a closure to pass data into and retrieve it from the callback doesn't look safe, but I can't see any other way to do it.
Any suggestions?
Here's a pattern that hopefully satisfies your needs:
import asyncnet, asyncdispatch
proc doStuff(): Future[string] {.async.} =
var server = newAsyncSocket()
server.bindAddr(Port(12345))
server.listen()
let client = await server.accept()
result = await client.recvLine
server.close()
let msg = waitFor(doStuff())
echo "message: ", msg
And here's a client that you can test it with:
import asyncnet, asyncdispatch
let client = newAsyncSocket()
waitFor client.connect("127.0.0.1", 12345.Port)
waitFor client.send("HELLO" & "\c\L")
Is it actually supposed to be a server rather than a client?
You can of course not use async, but why is it so bad to simply use await or waitFor at the call site?
Is it actually supposed to be a server rather than a client?
It's supposed to be both: receive a request, send a request to another host, and receive the response.
You can of course not use async,
In fact, that would be the thing I need. I don't mind blocking while waiting for responses. But I couldn't find an blocking HTTP server in the stdlib, and implementing it myself with sockets sounds like an overkill.
why is it so bad to simply use await or waitFor at the call site?
Could you please elaborate a bit on that? I don't mind doing something like waitFor acceptRequest. Is there a ready-to-use proc for that, or do you mean I should implement it by reading from socket?
Could you please elaborate a bit on that? I don't mind doing something like waitFor acceptRequest. Is there a ready-to-use proc for that, or do you mean I should implement it by reading from socket?
My example relies on a future, and waitFor, blocks until the future has finished. The same applies to the asynchttpserver interface, which can either be invoked in an asynchronous fashion with await (in an async proc), or in a blocking manner by passing sendHeaders for example to waitFor.
I would not block in the proc itself, because then you can't choose between the two methods, but this can be achieved by replacing Future[T] {.async.} with T and await with waitFor, at least in simple cases.
Does anything remain unclear?
Thanks for the suggestion, but I'd rather not do it this way, because the API will suffer. When I call getAccessCode from an external module, I'm expecting to get the access code in return, not a Future that should be awaited on. So, one way or the other, I need a regular function returning a regular string.
That's not how you should design such APIs. Doing it this way means that the caller has no choice but to block. If you return a Future then the caller can at least decide whether to run the code asynchronously or not.
I'm doing some work with Google Drive API. To obtain the access token, I have to receive a request, make another request, and extract data from it. So I need a proc that would start a server, receive a request, do a request, receive another request, shut down the server, and return the response. I'm using the standard asynchttpserver and httpclient modules.
Are you sure you need both a server and a client for this? It seems over complicated to me but I don't know how the Google Drive API works.
This method works just fine, but I don't like it for its explicitness. Also, using a closure to pass data into and retrieve it from the callback doesn't look safe, but I can't see any other way to do it.
If you really want to keep your semantics then your procedure seems fine. But instead of using the closure to pass data into it you should just define more parameters in your callback proc and pass the data as arguments.
So the tricky part for me is to receive one HTTP request.
What sort of application are you planning to use this in?
I have a feeling that it would make more sense to simply keep the server running instead of shutting it down and restarting it for each request.
I thought the same @dom96 but I did not post it because obviously a running server is one attack vector more in a system if you otherwise can be "closed".
My Hubic2SwiftGate is a program which does something similar:
The the whole server is used to answer a callback for OAuth2. But in my case it also is a front end for the authorization request because the original clients don't know how to handle this. Besides that it does not even proxy any requests after the auth-token is received. The remaining communication happens between different endpoints.
The only reason why It is running "all the time" is because it needs to be there whenever any client needs an auth token. This was written with PHP and needs a web-server installed but it is obviously a candidate for recoding it with Nim eventually. I do not use it as much as I thought but others do and for long term archiving 10 TB storage for 5 Euro per month is still a very good offering :)
@dom68 @OderWat The use case is pretty simple: I just need to upload a docx file to Google Drive from a CLI tool. The tool was originally written in Python with PyDrive, so the whole API interaction layer was hidden.
I also thought about keeping the server running the whole time, but this looks overcomplicated for the task.
I ended up writing an async proc the reads a single line from a socket as suggested by @ephja. It's not perfect, because I have to extract the code from the awaited line, e.g. "GET /?code=123qwe456asd789zxc HTTP/1.1" instead of using an existing HTTP parser. Also, I couldn't yet figure out how to send a proper HTTP response, but that's optional, so I didn't spend too much time on it.
I want to thank everybody for help! You guys are awesome!