nim-json-rpc/json_rpc/clients/httpclient.nim

160 lines
5.0 KiB
Nim
Raw Normal View History

import
std/[strutils, tables, uri],
stew/[byteutils, results],
chronos/apps/http/httpclient as chronosHttpClient,
chronicles, httputils, json_serialization/std/net,
".."/[client, errors]
2018-07-14 07:51:54 +00:00
export
client
{.push raises: [Defect].}
2018-07-14 07:51:54 +00:00
logScope:
topics = "JSONRPC-HTTP-CLIENT"
2018-07-14 07:51:54 +00:00
type
HttpClientOptions* = object
httpMethod: HttpMethod
2018-07-14 07:51:54 +00:00
RpcHttpClient* = ref object of RpcClient
httpSession: HttpSessionRef
httpAddress: HttpResult[HttpAddress]
maxBodySize: int
2022-03-04 19:13:29 +00:00
getHeaders: GetJsonRpcRequestHeaders
2018-07-14 07:51:54 +00:00
const
MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets
2018-07-14 07:51:54 +00:00
2022-03-04 19:13:29 +00:00
proc new(
T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false,
getHeaders: GetJsonRpcRequestHeaders = nil): T =
let httpSessionFlags = if secure:
{
HttpClientFlag.NoVerifyHost,
HttpClientFlag.NoVerifyServerName
}
2021-11-22 13:09:13 +00:00
else:
{}
T(
maxBodySize: maxBodySize,
2022-03-04 19:13:29 +00:00
httpSession: HttpSessionRef.new(flags = httpSessionFlags),
getHeaders: getHeaders
)
2022-03-04 19:13:29 +00:00
proc newRpcHttpClient*(
maxBodySize = MaxHttpRequestSize, secure = false,
getHeaders: GetJsonRpcRequestHeaders = nil): RpcHttpClient =
RpcHttpClient.new(maxBodySize, secure, getHeaders)
method call*(client: RpcHttpClient, name: string,
params: JsonNode): Future[Response]
{.async, gcsafe.} =
doAssert client.httpSession != nil
if client.httpAddress.isErr:
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
2018-07-14 07:51:54 +00:00
2022-03-04 19:13:29 +00:00
var headers =
if not isNil(client.getHeaders):
client.getHeaders()
else:
@[]
headers.add(("Content-Type", "application/json"))
let
id = client.getNextId()
reqBody = $rpcCallNode(name, params, id)
var req: HttpClientRequestRef
var res: HttpClientResponseRef
defer:
# BEWARE!
# Using multiple defer statements in this function or multiple
# try/except blocks within a single defer statement doesn't
# produce the desired run-time code, so we use slightly bizzare
# code to ensure the exceptions safety of this function:
try:
var closeFutures = newSeq[Future[void]]()
if req != nil: closeFutures.add req.closeWait()
if res != nil: closeFutures.add res.closeWait()
if closeFutures.len > 0: await allFutures(closeFutures)
except CatchableError as err:
# TODO
# `close` functions shouldn't raise in general, but we first
# need to ensure this through exception tracking in Chronos
debug "Error closing JSON-RPC HTTP resuest/response", err = err.msg
req = HttpClientRequestRef.post(client.httpSession,
client.httpAddress.get,
body = reqBody.toOpenArrayByte(0, reqBody.len - 1),
headers = headers)
res =
try:
await req.send()
except CancelledError as e:
raise e
except CatchableError as e:
raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC.", parent: e)
2021-11-22 13:09:13 +00:00
if res.status < 200 or res.status >= 300: # res.status is not 2xx (success)
raise newException(ErrorResponse, "POST Response: " & $res.status)
debug "Message sent to RPC server",
address = client.httpAddress, msg_len = len(reqBody)
trace "Message", msg = reqBody
2021-11-22 13:09:13 +00:00
let resBytes =
try:
await res.getBodyBytes(client.maxBodySize)
except CancelledError as e:
raise e
2021-11-22 13:09:13 +00:00
except CatchableError as exc:
raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC.", parent: exc)
let resText = string.fromBytes(resBytes)
trace "Response", text = resText
2018-07-14 07:51:54 +00:00
# completed by processMessage - the flow is quite weird here to accomodate
# socket and ws clients, but could use a more thorough refactoring
2018-07-14 07:51:54 +00:00
var newFut = newFuture[Response]()
# add to awaiting responses
client.awaiting[id] = newFut
try:
# Might raise for all kinds of reasons
client.processMessage(resText)
finally:
# Need to clean up in case the answer was invalid
client.awaiting.del(id)
# processMessage should have completed this future - if it didn't, `read` will
# raise, which is reasonable
if newFut.finished:
return newFut.read()
else:
# TODO: Provide more clarity regarding the failure here
raise newException(InvalidResponse, "Invalid response")
proc connect*(client: RpcHttpClient, url: string) {.async.} =
client.httpAddress = client.httpSession.getAddress(url)
if client.httpAddress.isErr:
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
2018-07-14 07:51:54 +00:00
proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool) {.async.} =
var uri = Uri(
scheme: if secure: "https" else: "http",
hostname: address,
port: $port)
2021-11-22 13:09:13 +00:00
let res = getAddress(client.httpSession, uri)
if res.isOk:
client.httpAddress = res
else:
raise newException(RpcAddressUnresolvableError, res.error)
2022-02-02 17:51:04 +00:00
method close*(client: RpcHttpClient) {.async.} =
if not client.httpSession.isNil:
await client.httpSession.closeWait()