Using the WinInet API to access the internet from Visual Basic (and Microsoft Office)

Introduction

It’s quite common for people to ask whether they can send data to the Internet (or Intranet) from, say, Excel, or Word. To which I frequently answer – yes – they can do it. But the problem is that there are a variety of ways this can be done, and that what’s possibly the best-supported way (using WinInet) is by no means trivial. Hence this note.

Other ways? If you understand http and Winsock (not as tricky as it sounds) and are familiar with converting Windows API calls for use in VB, then you can drive the server directly. But WinInet overcomes that need. So lets dive into some detail in that area.

Declarations

To use Windows API calls in any VB (or VBA) program, they have to be included in the declarations section at the top of the program. The declaration looks something like this.

Private Declare Function FuncName Lib "libname" Alias "AliasName" (ByVal variable)

FuncName is the name you use to refer to the function, libname is the windows dll (which will be WinInet in our example) and AliasName is the name of the function in the Windows API.

Connecting to a Web server

There’s a standard set of calls that every access to the Internet must make: start with InternetOpen, then call InternetConnect, and then HttpOpenRequest. Let’s look at each in turn. InternetOpen is declared as

Private Declare Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" ( _
    ByVal lpszAgent As String, _
    ByVal dwAccessType As Long, _
    ByVal lpszProxyName As String, _
    ByVal lpszProxyBypass As String, _
    ByVal dwFlags As Long) As Long

I’m not going to go into all of the options in too much detail (read the help files if you want to understand the lot) but I’d just note that we need to use the Visual Basic variable vbNullString where we’d normally use a C NULL. My standard use of InternetOpen is:

Dim hInternet As Long
hInternet = InternetOpen(App.Title, INTERNET_OPEN_TYPE_DIRECT, vbNullString, vbNullString, 0)

and we put Const INTERNET_OPEN_TYPE_DIRECT = 1 in the declarations section. Now – InternetConnect:

Private Declare Function InternetConnect Lib "wininet.dll" Alias "InternetConnectA" ( _
    ByVal hInternetSession As Long, _
    ByVal lpszServerName As String, _
    ByVal nServerPort As Integer, _
    ByVal lpszUsername As String, _
    ByVal lpszPassword As String, _
    ByVal dwService As Long, _
    ByVal dwFlags As Long, _
    ByVal dwContext As Long) As Long

Key things to know about this. hInternetSession is the return value of InternetOpen. lpszServerName is the name of the web server you’re trying to access (for instance, it could be "www.bt.com"). nServerPort is the TCP port you want to access, and would usually be 80 for the internet. I use Const INTERNET_SERVICE_HTTP = 3 as dwService. A standard call:

Dim hConnect As Long
hConnect = InternetConnect(hInternet, strServer, iPort, "", "", INTERNET_SERVICE_HTTP, 0, 0)

Finally, HttpOpenRequest.

Private Declare Function HttpOpenRequest Lib "wininet.dll" Alias "HttpOpenRequestA" ( _
    ByVal hHttpSession As Long, _
    ByVal lpszVerb As String, _
    ByVal lpszObjectName As String, _
    ByVal lpszVersion As String, _
    ByVal lpszReferer As String, _
    ByVal lpszAcceptTypes As String, _
    ByVal dwFlags As Long, _
    ByVal dwContext As Long) As Long

Much of this needs a quick explanation. hHttpSession is the value returned from InternetConnect. lpszVerb is either "GET" or "POST", depending (roughly) on whether you’re getting information from the internet, or sending it. If it’s the latter, it’s a bit more complex and we’ll cover it later. For now, assume GET. lpszObjectName is the URL you’re wanting to download (e.g. "\index.htm"), lpszVersion is the version of http you want to use – no reason for not using the value below. Be a bit careful with lpszAcceptTypes – this is a list of the mime types you’re willing to accept. However, it’s defined as a set of null terminated strings followed by a further null at the end. I don’t know how to do this in VB. Just putting, say, "text/html" for this value will lead to incorrect data being sent, since there will be no final null. As below, I set it to a null string. dwFlags also needs careful treatment. I’m not going to go into more detail here, but if you want to send cookies (using HttpAddRequestHeaders) you MUST set the flag equal to INTERNET_FLAG_NO_COOKIES (weird, huh). There are also "gotchas" around the way that WinInet caches returned values. For most applications, you should OR that value with INTERNET_FLAG_NO_CACHE_WRITE. So in the declares we have:

Const INTERNET_FLAG_NO_COOKIES = &H80000
Const INTERNET_FLAG_NO_CACHE_WRITE = &H4000000

In the body of the program, we’d see something like:

Dim lFlags As Long
Dim hRequest As Long

lFlags = INTERNET_FLAG_NO_COOKIES
lFlags = lFlags Or INTERNET_FLAG_NO_CACHE_WRITE

hRequest = HttpOpenRequest(hConnect, "GET", strURL, "HTTP/1.0", vbNullString, vbNullString, lFlags, 0)

Sending the request and reading the response

OK – now we’ve opened a path to the server. All we now need to do is send the request and read what the server sends back. Sending is done with HttpSendRequest.

Private Declare Function HttpSendRequest Lib "wininet.dll" Alias "HttpSendRequestA" ( _
    ByVal hHttpRequest As Long, _
    ByVal lpszHeaders As String, _
    ByVal dwHeadersLength As Long, _
    ByVal lpOptional As String, _
    ByVal dwOptionalLength As Long) As Boolean

You’ll get used to this soon. hHttpRequest is the return from HttpOpenRequest. We’ll talk about the rest in a bit, but for now they can be set to NULL or zero.

Dim bRes As Boolean

bRes = HttpSendRequest(hRequest, vbNullString, 0, vbNullString, 0)

Reading the result is done with InternetReadFile.

Private Declare Function InternetReadFile Lib "wininet.dll" ( _
    ByVal hFile As Long, _
    ByVal lpBuffer As String, _
    ByVal dwNumberOfBytesToRead As Long, _
    ByRef lpNumberOfBytesRead As Long) As Boolean

This puts the returned data into lpBuffer, a string buffer. There’s obviously lots of ways of saving this data, but I tend to limit the buffer to 1 byte (poor for performance, but good to ensure that you accurately capture all the returned data) and write this to file. Another VB gotcha is the need to delete the file before opening it for writing, otherwise it just overwrites the data at the start of the file, leaving the rest as was. Sample code (variables declared only where they’re not obvious).

Dim strBuffer As String * 1
strDir = Dir(App.Path & "\" & strFile)
If Len(strDir) > 0 Then
    Kill App.Path & "\" & strFile
End If

iFile = FreeFile()
Open App.Path & "\" & strFile For Binary Access Write As iFile

Do
    bRes = InternetReadFile(hRequest, strBuffer, Len(strBuffer), lBytesRead)
    If lBytesRead > 0 Then
        Put iFile, , strBuffer
    End If
Loop While lBytesRead > 0

That’s about it for reading a Web page. What about sending data to one?

Sending data

There are actually 2 ways of sending data to a webserver – extending the URL in a GET, or using POST. The former is generally OK for small amounts of data (less than a few hundred bytes), but is unreliable for more data. (I’ve spent some time looking at this, and concluded that part of the problem lies in the size of packet that is sent by the TCP protocol – which means that, even if you can use GET to send, say, 2kBytes of data on a LAN, it won’t always work over dial up!).

Anyway – to send data using GET, all you need to do is add a question mark to the URL and then the form data (with each field separated by an &) – e.g. the URL would become index.asp?FormField=value&Field2=value2. (There’s a gotcha here with asp’s – they’ll only accept form data where the mime type is set to "Content-Type: application/x-www-form-urlencoded", which causes some problems with the mime type stuff I put above – but I’ll just flag the issue here, not solve it!).

So, it’s better to use POST. What differences do we need to make to what we said above? Firstly, HttpOpenRequest obviously uses POST, not GET, so it becomes:

hRequest = HttpOpenRequest(hConnect, "POST", sURL, "HTTP/1.0", vbNullString, vbNullString, 0, 0)

The POST data is created as above, with field name/value pairs, separated by & and is put into a query string which is sent as part of the HttpSendRequest, as follows:

bRes = HttpSendRequest(hRequest, vbNullString, 0, strPostData, Len(strPostData))

Other stuff

There’s quite a bit of other stuff I could go into about cookies, headers, returned status codes, etc. – but I’ll leave that for another day.

Distributing your application

It’s not clear from the M$ documentation, but WinInet isn’t a standard part of Windows ’95 – it was actually delivered as part of Internet Explorer. You’ve therefore got to be prepared to send out WinInet.dll with your program, if you distribute it. But unfortunately, Wininet depends on 2 other dll’s – shlwapi.dll and advapi32.dll, so you need to be ready to send them out too. Problems here are that you can’t just copy those dll’s to windows/system, because they may already be there and in use. Also, advapi32 comes in NT and ’95 specific versions, and if you install the wrong one, it will crash Windows and implies a reinstall. My advice would be to suggest users install IE5, unless you are using a professional installer and know what you’re doing!

Phil Holmes, 2000