System.Net.WebException 从 C# 移植到 F# 时

System.Net.WebException when porting from C# to F#

我正在尝试将一些 C# 代码移植到 F#。

C# 代码取自此处(并略微剥离):https://github.com/joelpob/betfairng/blob/master/BetfairClient.cs

    public bool Login(string p12CertificateLocation, string p12CertificatePassword, string username, string password)
    {

        var appKey = "APPKEY";
        string postData = string.Format("username={0}&password={1}", username, password);
        X509Certificate2 x509certificate = new X509Certificate2(p12CertificateLocation, p12CertificatePassword);
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://identitysso.betfair.com/api/certlogin");
        request.UseDefaultCredentials = true;
        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        request.Headers.Add("X-Application", appKey);
        request.ClientCertificates.Add(x509certificate);
        request.Accept = "*/*";


        using (Stream stream = request.GetRequestStream())
        using (StreamWriter writer = new StreamWriter(stream, Encoding.Default))

        writer.Write(postData);


        using (Stream stream = ((HttpWebResponse)request.GetResponse()).GetResponseStream())
        using (StreamReader reader = new StreamReader(stream, Encoding.Default))

上面的 C# 代码效果很好。但是,当尝试 运行(我认为是)F# 等效代码时,没有任何实际更改,我收到一条错误消息。

代码来自 运行 来自同一台计算机、相同的 VS 安装和完全相同的 4 个参数。

我收到的错误消息在倒数第二行:

member x.Login(username, password,p12CertificateLocation:string, p12CertificatePassword:string) = 
    let AppKey = "APPKEY"
    let  url = "https://identitysso.betfair.com/api/certlogin"
    let postData =  "username=" + username + "&password=" + password
    let x509certificate = new X509Certificate2(p12CertificateLocation, p12CertificatePassword)

    let req = HttpWebRequest.Create(url) :?> HttpWebRequest 
    req.ClientCertificates.Add(x509certificate)|>ignore
    req.UseDefaultCredentials <- true
    req.Method <- "POST"
    req.ContentType <- "application/x-www-form-urlencoded"
    req.Headers.Add("X-Application",AppKey)
    req.Accept <-"*/*" 

    use stream = req.GetRequestStream()
    use writer =new StreamWriter(stream,Encoding.Default)                      
    writer.Write(postData)

    // fails on this line:
    use stream = (req.GetResponse()  :?> HttpWebResponse ).GetResponseStream()
    // with System.Net.WebException: 'The remote server returned an error: (400) Bad Request.'
    use reader = new StreamReader(stream,Encoding.Default)

我有点迷路了,我觉得这两个代码实现应该是相同的?

在此 C# 代码中:

using (Stream stream1 = request.GetRequestStream())
using (StreamWriter writer = new StreamWriter(stream1, Encoding.Default))
    writer.Write(postData);

using (Stream stream2 = ((HttpWebResponse)request.GetResponse()).GetResponseStream())
using (StreamReader reader = new StreamReader(stream2, Encoding.Default))

writerstream1writer.Write 调用完成后立即刷新并关闭,然后再调用 request.GetResponse()。 (由于,呃.. 有趣 你的代码格式,这个事实有点模糊。)

在此 F# 代码中:

use stream1 = req.GetRequestStream()
use writer = new StreamWriter(stream1, Encoding.Default)
writer.Write(postData)

use stream2 = (req.GetResponse() :?> HttpWebResponse).GetResponseStream()
use reader = new StreamReader(stream2, Encoding.Default)
当调用 req.GetResponse() 时,

writerstream1 保持活动状态并保持未刷新和未关闭状态;您需要将它们放在人工范围内以获得与 C# 相同的行为:

do  use stream1 = req.GetRequestStream()
    use writer = new StreamWriter(stream1, Encoding.Default)
    writer.Write(postData)

(* or

(use stream1 = req.GetRequestStream()
 use writer = new StreamWriter(stream1, Encoding.Default)
 writer.Write(postData))

*)

use stream2 = (req.GetResponse() :?> HttpWebResponse).GetResponseStream()
use reader = new StreamReader(stream2, Encoding.Default)

这不是 "the C# way" 进行 HTTP POST 调用。在所有受支持的 .NET 版本(即 4.5.2 及更高版本)中,典型的方法是使用 HttpClient。即使使用 HttpWebRequest,也有太多冗余或矛盾的调用,例如使用默认凭据(即 Windows 身份验证)

C#方式是这样的:

var client=new HttpClient("https://identitysso.betfair.com/api");
var values = new Dictionary<string, string>
{
   { "username", username },
   { "password", password }
};

var content = new FormUrlEncodedContent(values);
content.Headers.Add("X-Application",apiKey);

var response = await client.PostAsync("certlogin", content);
var responseString = await response.Content.ReadAsStringAsync();    

为了使用客户端证书,您必须使用自定义 HTTP 处理程序创建客户端实例:

var handler = new WebRequestHandler();
var x509certificate = new X509Certificate2(certPath, certPassword);
handler.ClientCertificates.Add(certificate);
var client = new HttpClient(handler)
             {
                 BaseAddress = new Uri("https://identitysso.betfair.com/api")
             }

在 F# 中编写相同的代码非常简单:

let login username password (certPath:string) (certPassword:string) (apiKey:string) = 
    let handler = new WebRequestHandler()
    let certificate = new X509Certificate2(certPath, certPassword)
    handler.ClientCertificates.Add certificate |> ignore
    let client = new HttpClient(handler,BaseAddress = Uri("https://identitysso.betfair.com"))

    async {    
        let values = dict["username", username ; "password", password ] 
        let content = new FormUrlEncodedContent(values)
        content.Headers.Add( "X-Application" ,apiKey)    

        let! response = client.PostAsync("api/certlogin",content) |> Async.AwaitTask
        response.EnsureSuccessStatusCode() |> ignore
        let! responseString = response.Content.ReadAsStringAsync() |> Async.AwaitTask
        return responseString
    }

客户端、处理程序是线程安全的,可以重复使用,因此可以将它们存储在字段中。重复使用相同的客户端意味着 OS 不必每次都创建新的 TCP/IP 连接,从而提高性能。最好单独创建客户端。 :

let buildClient (certPath:string) (certPassword:string) =
    let handler = new WebRequestHandler()
    let certificate = new X509Certificate2(certPath, certPassword)
    handler.ClientCertificates.Add certificate |> ignore
    new HttpClient(handler,BaseAddress = Uri("https://identitysso.betfair.com"))


let login (client:HttpClient) username password  (apiKey:string) = 
    async {    
        let values = dict["username", username ; "password", password ] 
        let content = new FormUrlEncodedContent(values)
        content.Headers.Add( "X-Application" ,apiKey)    

        let! response = client.PostAsync("api/certlogin",content) |> Async.AwaitTask
        response.EnsureSuccessStatusCode() |> ignore
        let! responseString = response.Content.ReadAsStringAsync() |> Async.AwaitTask
        //Do whatever is needed here 
        return responseString
    }