Search code examples
c#httprequesthttpclientclearscript

Unable to perform http request asynchronously in ClearScript V8


I am attempting to make an http request and get the response back in ClearScript V8 in my C# controller. When I run the function synchronously, it works fine. But I would like to run it asynchronously to take advantage of non-blocking calls. However, when I attempt to do that, I just get a null response back. I created a couple of helper classes to perform the http client get, and added those to the ClearScript engine. And as stated, running in sync mode, it works great. But when (attempting to ) run on async mode, it just comes back null. Am I even thinking of this correctly? Is this even possible? Or are we locked in because of limitations in ClearScript? Any help is appreciated. Below if my test stub controller:

public class Javascript : Controller
{
    public async Task<IActionResult> Index()
    {
        var hostVariables = new HostVariables();
        V8ScriptEngine engine = new V8ScriptEngine();
        engine.DocumentSettings.AccessFlags = DocumentAccessFlags.None;
        engine.AddHostType(typeof(Console));
        using (HttpClient client = new HttpClient())
        {
            var request = new Request(client);
            engine.AddHostObject("hostVariables", hostVariables);
            engine.AddHostObject("request", request);
            engine.Execute(new DocumentInfo() { Category = ModuleCategory.Standard }, @"
                hostVariables.syncResponse = request.get(""https://www.google.com"").responseText;
                hostVariables.asyncResponse = (await request.getAsync(""https://www.google.com"")).responseText;
            ");
        }

        //hostVariables.syncResponse contains the Google website html
        //hostVariables.asyncResponse is null

        return View();
    }
}
public class HostVariables
{
    public string syncResponse { get; set; }
    public string asyncResponse { get; set; }
}
//HELPER CLASSES TO GIVE CLEARSCRIPT THE ABILITY TO PERFORM HTTP REQUESTS
public class Request
{
    private HttpClient _client;
    public Request(HttpClient client)
    {
        _client = client;
    }
    //ASYNC AND SYNC MODE ARE EXACTLY THE SAME, EXCEPT FOR THE SYNC/ASYNC METHODS
    public Response get(string url)
    {
        var response = new Response();
        var getResponse = _client.GetAsync(url).Result;
        var encoding = ASCIIEncoding.ASCII;
        response.statusCode = (int)getResponse.StatusCode;
        if (getResponse.Content != null)
            response.responseText = getResponse.Content.ReadAsStringAsync().Result;
        if (getResponse.Headers != null)
            response.responseHeaders = getResponse.Headers.Select(x => new ResponseHeader() { key = x.Key, value = x.Value.ToArray() }).ToArray();
        return response;
    }
    public async Task<Response> getAsync(string url)
    {
        var response = new Response();
        var getResponse = await _client.GetAsync(url);
        var encoding = ASCIIEncoding.ASCII;
        response.statusCode = (int)getResponse.StatusCode;
        if (getResponse.Content != null)
            response.responseText = await getResponse.Content.ReadAsStringAsync();
        if (getResponse.Headers != null)
            response.responseHeaders = getResponse.Headers.Select(x => new ResponseHeader() { key = x.Key, value = x.Value.ToArray() }).ToArray();
        return response;
    }
}
public class Response
{
    public string? responseText;
    public ResponseHeader[]? responseHeaders;
    public int? statusCode;
}
public class ResponseHeader
{
    public string? key;
    public string[]? value;
}

Solution

  • There are two problems with your sample.

    First, to allow JavaScript code to await .NET tasks in the desired manner, use V8ScriptEngineFlags.EnableTaskPromiseConversion.

    Second, you need to await your JavaScript code just as you would await a .NET async method. Otherwise, you're just kicking off the operation and resuming Index execution before it completes.

    Here's a working Index:

    public async Task<IActionResult> Index() {
        var hostVariables = new HostVariables();
        using var engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableTaskPromiseConversion);
        using var client = new HttpClient();
        var request = new Request(client);
        engine.AddHostObject("hostVariables", hostVariables);
        engine.AddHostObject("request", request);
        await (Task)engine.Evaluate(new DocumentInfo() { Category = ModuleCategory.Standard }, @"
            hostVariables.syncResponse = request.get(""https://www.google.com"").responseText;
            hostVariables.asyncResponse = (await request.getAsync(""https://www.google.com"")).responseText;
        ");
        return Content(hostVariables.asyncResponse);
    }
    

    BTW, if you need to re-execute your JavaScript code, you could wrap it in an async function instead of a module:

    dynamic func = engine.Evaluate(@"(async function () {
        hostVariables.syncResponse = request.get(""https://www.google.com"").responseText;
        hostVariables.asyncResponse = (await request.getAsync(""https://www.google.com"")).responseText;
    })");
    await func();
    

    Note that func becomes invalid when engine is disposed.