Search This Blog

Friday, July 22, 2011

WinHttpSendRequest is not so easy to use

The other day I tried to look up WinHttpSendRequest() API for more information and found that the second entry in google search result was my question that I posted at MSDN forum two years ago with regard to its usage example.
At that time I was trying to understand how to use WinHttpSendRequest() API to construct Http POST request and could not find any good example on the web and hence, I decided that I should write something about it in my blog.

Followings are the basic flow of WinHttp calls to create Http Post request.

  • Prepare WinHttp session handle and connection handle. We can reuse these two handles to send multiple requests. For this use WinHttpOpen and WinHttpConnect API
  • Create a specific WinHttp request handle. Once we have the request handle, we can associate certain options with this handle. Use WinHttpOpenRequest.
  • Construct POST request and details are as follows. This is where it gets real tricky.

In order to successfully send our Http Request, we need two APIs: WinHttpAddRequestHeaders() and WinHttpSendRequest()

First, I assume that we want to send multipart form HTTP Post request to upload a file along with some fields. For this task, we need to specify our intention but what's tricky is the fact that we need to specify the boundary of http header so that Web server knows where the header ends and how to parse our request. Here is an example of its usage.

WinHttpAddRequestHeaders(
    hRequest, 
    "Content-Type:multipart/form-data; boundary=----------ThIs_Is_tHe_bouNdaRY_$", 
    -1L, 
    WINHTTP_ADDREQ_FLAG_ADD
);

As you can see, we have put certain string such as "boundary=----------ThIs_Is_tHe_bouNdaRY_$" to inform the web server to look for these string to parse the request correctly.

Now, it is time to construct all the fields. Let me give you one example of form field.
Say we have a field called "username" and want to specify its value "ilho". Following is the sequence of string we need to construct and just to make it easy to read, I added 'new line' after '\r\n' but in reality you need to have all the strings together as one string. You need to add the same format for all the fields to the same string.

------------ThIs_Is_tHe_bouNdaRY_$\r\n
Content-Disposition: form-data; name="username"\r\n\r\n
ilho\r\n

For data file that we want to upload, we need special care. Now, assume that we want to upload some text file. Here is the string that you want to build.

------------ThIs_Is_tHe_bouNdaRY_$\r\n
Content-Disposition: form-data; name="personal_data";\r\n filename="personal.txt"\r\n
Content-Type: text/plain; charset=utf-8\r\n\r\n

Finally, you will need to end your request by specifying the ending as follows.

\r\n------------ThIs_Is_tHe_bouNdaRY_$--\r\n\r\n

I know that these look really ugly and I do not know if there is any better way to do this. Unless I am mistaken, I think for this specific part, I prefer to use curl because it is a lot easier to use curl to construct POST request than WinHttp.

But for now, let me throw out some code example to illustrate what we need.

StringCbPrintf(
    ffield,
    sizeof(ffield),
    "------------ThIs_Is_tHe_bouNdaRY_$\r\n"
    "Content-Disposition: form-data; name="username"\r\n\r\n"
    "ilho\r\n"
);

StringCbPrintf(
    filefield,
    sizeof(filefield),
    "------------ThIs_Is_tHe_bouNdaRY_$\r\n"
    "Content-Disposition: form-data; name=\"personal_data\"; filename=\"personal.txt\"\r\n"
    "Content-Type: text/xml; charset=utf-8\r\n\r\n"
);

StringCbPrintf(formdata, sizeof(formdata), "%s%s", ffield, filefield);

Once we have the form ready, we need to calculate the entire size of request which includes the actual text file. With all these information, here is WinHttpSendRequest.

total_length = strlen(formdata) + filesize + strlen("\r\n------------ThIs_Is_tHe_bouNdaRY_$--\r\n\r\n");


WinHttpSendRequest(
    hRequest,
    WINHTTP_NO_ADDITIONAL_HEADERS,
    0,
    (LPVOID)formdata,
    (DWORD)strlen(formdata),
    total_length,
    0
);

Once we send our HTTP request, we can start to send our text file using WinHttpWriteData followed by boundary string to finish the HTTP Post request. Hence, in minimum we need to call two WinHttpWriteData.

WinHttpWriteData(file content);
WinHttpWriteData("\r\n------------ThIs_Is_tHe_bouNdaRY_$--\r\n\r\n");  // boudary

At this point, we just need to receive the Web server response to your request by calling WinHttpReceiveResponse() API and we are pretty much done by now.

In the above example, I tried to present the steps in a rather raw format but in reality you may want to create either inline functions or macros to to reduce the typings and create a nice interface to work with WinHttp. Like I said, if you need to work on the similar code for both Windows and Linux, it is better off to use curl and I hope that MS will come up with better APIs or interfaces for the developers to work with Http requests.

8 comments:

  1. Hi, I followed your pose to upload a doc file via http Post request, but got windows error 87 in httpSendRequest function. The error is WINDOWS_ALREADY_EXISTS. The headers I sent in WinHttpAddRequestHeaders are:

    Host: www.xxx.com
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101 Firefox/13.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-us;q=0.5
    Accept-Encoding: gzip, deflate
    Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Keep-Alive: 115
    Connection: keep-alive
    Content-Length: 32
    Pragma: no-cache
    Cache-Control: no-cache
    Content-Type: multipart/form-data; boundary=----------ThIs_Is_tHe_bouNdaRY_$

    And the form data part is:
    ------------ThIs_Is_tHe_bouNdaRY_$
    Content-Disposition: form-data; name="struts.enableJSONValidation"

    true

    ------------ThIs_Is_tHe_bouNdaRY_$
    Content-Disposition: form-data; name="upload"; fileName="CloudDoc.docx"
    Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
    Content-Transfer-Encoding: binary

    I got the error when I sent the form data part in WinHttpSendRequest. Do you have any idea about what is wrong with my code?

    Thanks a lot!!!

    ReplyDelete
  2. Error code 87 means "ERROR_INVALID_PARAMETER" according to http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx

    So I suppose that some of params must be set incorrectly when you call WinHttpSendRequest() API. Can you check and see if they are correct. Especially check and see if the length is correct.

    ReplyDelete
    Replies
    1. thank you very much for your reply.
      I can upload the file and return successfully now, but somehow the size of the docx file is not correct. And the content in the file is said damaged or something that I cannot open the downloaded file.
      I think it must be related to the content length and format. I uploaded the content in raw bytes data. Do I need to encode it in another format? How to calculate the content length of it. I have been struggled with this problem for several days. Really appreciate your help!

      Delete
  3. This is something that I have not tried so I could be wrong but checking online seems to suggest that your posted content-type is correct. For file size, I suppose that you can just use GetFileSizeEx() API to query the file size and use that value to get the total size. The other thing to verify is that open up the uploaded file in hex editor to see either the beginning or end of the file to see if there is any garbage character in comparison to the original one. If you find something, maybe you can work your way up to the root cause.

    ReplyDelete
  4. Hi, I am able to connect connect successfully and on WinHttpSendRequest returns success. When I am trying to get the response using "WinHttpReceiveResponse", getting error "ERROR_WINHTTP_CONNECTION_ERROR". Any help will be appreciated.
    Amresh Kumar

    ReplyDelete
  5. The leading hyphen's in the boundary string of the given example are a little confusing. To clean the confusion: there must be 2 hyphens before the boundary string (boundary string can be whatever that is mentioned in the WinHttpAddRequestHeaders) to convey the start of the boundary, 2 more hyphen's at the end to convey the end of the boundary.

    Example:
    //My boundary here is the string "SACHINISTHEBESTPLAYERINTHEWORLD".

    WinHttpAddRequestHeaders(hRequest, L"content-type: multipart/form-data; boundary=SACHINISTHEBESTPLAYERINTHEWORLD",-1L, WINHTTP_ADDREQ_FLAG_ADD)))

    //Now, I will use the string to specify the start and end of my multipart formdata (as below).

    char username[1024] = "Rohit";
    wsprintfA(preData, "%s%s%s", "--SACHINISTHEBESTPLAYERINTHEWORLD\r\nContent-Disposition: form-data; name=\"username\"\r\n\r\n", username, "\r\n--SACHINISTHEBESTPLAYERINTHEWORLD--\r\n\r\n");

    ReplyDelete
  6. Thank you - sir
    after 2 days of searching for an answer found it in your blog

    ReplyDelete
  7. hi there,
    if I want to send a json data instead of a file for example, how would that be done?

    ReplyDelete