Skip to content

Instantly share code, notes, and snippets.

@CandleCandle
Last active May 30, 2017 09:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CandleCandle/260a284ece6d2e99520cfbd99798e739 to your computer and use it in GitHub Desktop.
Save CandleCandle/260a284ece6d2e99520cfbd99798e739 to your computer and use it in GitHub Desktop.
HTTPClient performing multiple requests

Problem

Examples and docs say:

  • Create a HTTPClient "A client should create one instance of this class."[0]
  • Create a Payload
  • Create a HandlerMaker
  • Call client.apply(payload, handlermaker)

apply(..) creates the TCP connection and enables TLS as appropriate; wrapping the handler (created by the provided HandlerMaker) in a _ClientConnection which is the HTTPSession. The client then caches this HTTPSession against the (scheme, host, port) tuple.

Now, we have a Map in the HTTPClient from (scheme, host, port) ==> (tcp_connection, response_handler) The first request goes through and is handled happily by the response handler.

Now we want to make another call to the same http service; but with a different path/parameters.

We create a Payload, and a different HandlerMaker - we want to do something different with the result of the second call as the data that is returned is completely different. Trigger the request by calling client.apply(new_payload, new_handler_maker)

The client then notes the there is a mapping for the existing (scheme, host, port) tuple and reuses the session from before. This is a mostly a good thing as we now do not need to re-setup a TLS connection, removing one of the notable https performance overheads, HOWEVER, it also re-uses the response_handler for the FIRST request as it has not touched the new_handler_maker at all.

Given the code attached, this produces an infinite loop; as it makes the request for the second request, but calls the callback for the first request; causing the second request to be re-made, and never calling _Runner.run(...).

Solutions

  1. in the handler (or somewhere), maintain a map/stack/? of Payload URLs and resolve the correct handler when the response is received. This is suboptimal as I predict there will be a whole load of data races as adding things to the various actors' state can happen out-of-order
  2. create a new HTTPClient for every request, and therefore negate the connection re-use advantage.
  3. re-instate the handler field in the Payload class, and get the _Responder to call it when it's done.

Discussion

All the examples that I've seen are either prior to the change [1] in net/http or only make one http request and then exit [1,2].

Footnotes

use l = "logger"
use "net/http"
interface Notify
fun apply(str: String val)
actor ApiHttp
let _base: String = "http://example.com/some-api"
let _client: HTTPClient iso
let _log: l.Logger[String]
new create(log': l.Logger[String], client': HTTPClient iso) =>
_log = log'
_client = consume client'
be get_data(name: String, notify: Notify iso) =>
_log(l.Info) and _log.log("request for param: " + name)
let url_str = _base + "systems.php?name=" + name
let url = try
URL.valid(url_str)
else
_log(l.Error) and _log.log("invalid url: " + url_str)
return
end
let responder = recover val _ResponderFactory.create(_log, consume notify) end
_log(l.Info) and _log.log("full url: " + url.string())
let req = Payload.request("GET", url)
req("User-Agent") = "Ponylang"
try
let sendreq = _client(consume req, responder)
for (k, v) in sendreq.headers().pairs() do
_log(l.Info) and _log.log("<< " + k + ": " + v)
end
else
_log(l.Error) and _log.log("?? " + url_str)
end
class _ResponderFactory is HandlerFactory
let _notify: Notify val
let _log: l.Logger[String]
new create(log: l.Logger[String], notify: Notify val) =>
_log = log
_notify = notify
_log(l.Info) and _log.log("creating a _ResponderFactory")
fun apply(session: HTTPSession): HTTPHandler ref^ =>
_log(l.Info) and _log.log("creating a _Responder")
_Responder.create(session, _log, _notify)
class _Responder is HTTPHandler
let _notify: Notify val
let _session: HTTPSession
let _log: l.Logger[String]
var _body_buf: String ref
new create(session: HTTPSession, log: l.Logger[String], notify: Notify val) =>
_log = log
_notify = notify
_session = session
_body_buf = String.create(300)
fun ref apply(response: Payload val) =>
_log(l.Info) and _log.log("response -- status: " + response.status.string())
_log(l.Info) and _log.log("response -- method: " + response.method)
for (k, v) in response.headers().pairs() do
_log(l.Info) and _log.log(">> " + k + ": " + v)
end
match response.transfer_mode
| OneshotTransfer =>
let str = recover trn String.create(300) end
try
for piece in response.body().values() do
str.append(piece)
end
_log(l.Info) and _log.log("one shot str len: " + str.size().string())
if str.size() > 0 then
_notify(consume str)
end
else
_log(l.Error) and _log.log("response body err")
end
| StreamTransfer | ChunkedTransfer =>
_log(l.Info) and _log.log("stream or chunked transfer.")
end
fun ref chunk(data: ByteSeq val) =>
_log(l.Info) and _log.log("data chunk size: " + data.size().string())
_body_buf.append(data)
fun ref finished() =>
_log(l.Info) and _log.log("finished")
_session.dispose()
_notify(_body_buf.clone())
_body_buf.clear()
fun ref cancelled() =>
_log(l.Info) and _log.log("cancelled")
actor Main
new create(env: Env) =>
let opts = Options(env.args)
opts
.add("a", "a", StringArgument)
.add("b", "b", StringArgument)
.add("verbose", "v", None)
var a: String = ""
var b: String = ""
var level: LogLevel = Warn
for option in opts do
match option
| ("a", let a': String) => a = a'
| ("b", let b': String) => b = b'
| ("verbose", let n: None) => level = Fine
| let err: ParseError =>
err.report(env.out)
return
end
end
let log = StringLogger(level, env.out)
let runner = _Runner.create(log)
try
let client = recover iso http.HTTPClient.create(env.root as AmbientAuth) end
let api_wrap = ApiHttp.create(log, consume client)
api_wrap.get_data(a, recover iso {(a_result: String)(runner) =>
log(Info) and log.log("found first info: " + a_result.string())
api_wrap.get_data(b, recover iso {(b_result: String)(runner) =>
log(Info) and log.log("found second info: " + b_result.string())
runner.run(a_result, b_result)
} end)
} end)
else
log(Error) and log.log("network unavailable.")
end
actor _Runner
let _log: Logger[String]
new create(log: Logger[String]) =>
_log = log
be run(a: String, b: String) =>
_log(Info) and _log.log("running...")
_log(Info) and _log.log("a: " + a.string())
_log(Info) and _log.log("b: " + b.string())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment