nim-json-rpc/json_rpc/clients/httpclient.nim
Etan Kissling 60c4c9b5f2
improve RPC client logging (#171)
We currently only debug log successfully sent messages to RPC.
For debugging, it would be better to see the attempt and any failures.
Adjust logging to provide more information when debugging.
Also include the message name at debug level instead of just trace.
2023-08-23 14:20:19 +02:00

169 lines
5.3 KiB
Nim

import
std/[tables, uri],
stew/[byteutils, results],
chronos/apps/http/httpclient as chronosHttpClient,
chronicles, httputils, json_serialization/std/net,
".."/[client, errors]
export
client, HttpClientFlag, HttpClientFlags
{.push raises: [Defect].}
logScope:
topics = "JSONRPC-HTTP-CLIENT"
type
HttpClientOptions* = object
httpMethod: HttpMethod
RpcHttpClient* = ref object of RpcClient
httpSession: HttpSessionRef
httpAddress: HttpResult[HttpAddress]
maxBodySize: int
getHeaders: GetJsonRpcRequestHeaders
const
MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets
proc new(
T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false,
getHeaders: GetJsonRpcRequestHeaders = nil, flags: HttpClientFlags = {}): T =
T(
maxBodySize: maxBodySize,
httpSession: HttpSessionRef.new(flags = flags),
getHeaders: getHeaders
)
proc newRpcHttpClient*(
maxBodySize = MaxHttpRequestSize, secure = false,
getHeaders: GetJsonRpcRequestHeaders = nil,
flags: HttpClientFlags = {}): RpcHttpClient =
RpcHttpClient.new(maxBodySize, secure, getHeaders, flags)
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)
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
template closeRefs() =
# We can't trust try/finally in async/await in all nim versions, so we
# do it manually instead
if req != nil:
try:
await req.closeWait()
except CatchableError as exc: # shouldn't happen
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
if res != nil:
try:
await res.closeWait()
except CatchableError as exc: # shouldn't happen
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
debug "Sending message to RPC server",
address = client.httpAddress, msg_len = len(reqBody), name
trace "Message", msg = reqBody
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:
debug "Cancelled POST Request with JSON-RPC", e = e.msg
closeRefs()
raise e
except CatchableError as e:
debug "Failed to send POST Request with JSON-RPC", e = e.msg
closeRefs()
raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC: " & e.msg, parent: e)
if res.status < 200 or res.status >= 300: # res.status is not 2xx (success)
debug "Unsuccessful POST Request with JSON-RPC",
status = res.status, reason = res.reason
closeRefs()
raise (ref ErrorResponse)(status: res.status, msg: res.reason)
let resBytes =
try:
await res.getBodyBytes(client.maxBodySize)
except CancelledError as e:
debug "Cancelled POST Response for JSON-RPC", e = e.msg
closeRefs()
raise e
except CatchableError as e:
debug "Failed to read POST Response for JSON-RPC", e = e.msg
closeRefs()
raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC: " & e.msg, parent: e)
let resText = string.fromBytes(resBytes)
trace "Response", text = resText
# completed by processMessage - the flow is quite weird here to accomodate
# socket and ws clients, but could use a more thorough refactoring
var newFut = newFuture[Response]()
# add to awaiting responses
client.awaiting[id] = newFut
try:
# Might raise for all kinds of reasons
client.processMessage(resText)
except CatchableError as e:
# Need to clean up in case the answer was invalid
debug "Failed to process POST Response for JSON-RPC", e = e.msg
client.awaiting.del(id)
closeRefs()
raise e
client.awaiting.del(id)
closeRefs()
# 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
debug "Invalid POST Response for JSON-RPC"
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)
proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool) {.async.} =
var uri = Uri(
scheme: if secure: "https" else: "http",
hostname: address,
port: $port)
let res = getAddress(client.httpSession, uri)
if res.isOk:
client.httpAddress = res
else:
raise newException(RpcAddressUnresolvableError, res.error)
method close*(client: RpcHttpClient) {.async.} =
if not client.httpSession.isNil:
await client.httpSession.closeWait()