Today, irked at a particular site that hits my blog every 15 minutes, instead of the generally-accepted hour or more, I was reminded of the HTTP/1.1 If-Modified-Since header, which causes only the header to be returned if there has been no change to the requested file. Since I couldn’t tell if this worked on my site (I don’t have direct log file access on donnael.com), I decided to write some code to check the behavior myself.
While I created all of this code from the MSDN documentation, I have to give a nod to Rick Strahl’s wwIPStuff, which does this much better than I did (error checking? What’s that?), and which gave me a couple of pointers on how to properly make these calls.
*!* USERAGENT is more typically IE or Mozilla.
*!* BLOGSITE is just the name of the site, with no protocol
*!* RSSFEED works equally well without the leading slash,
*!* in this particular case.
#DEFINE USERAGENT "donnael ChangeCheck"
#DEFINE BLOGSITE "blog.donnael.com"
#DEFINE RSSFEED "/blog.xml"
*!* Defines further down in the code are taken from
*!* Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\WinInet.h
*!* Open a connection to the internet, but don't go
*!* anywhere.
DECLARE integer InternetOpen IN wininet.dll ;
string lpszAgent, ;
integer dwAccessType, ;
string lpszProxyName, ;
string lpszProxyBypass, ;
integer dwFlags
lnHNet = InternetOpen(USERAGENT, 0, "", "", 0)
*!* Now make a connection to the chosen site.
DECLARE integer InternetConnect IN wininet.dll ;
integer hInternet, ;
string lpszServerName, ;
integer nServerPort, ;
string lpszUsername, ;
string lpszPassword, ;
integer dwService, ;
integer dwFlags, ;
integer dwContext
#DEFINE INTERNET_SERVICE_HTTP 3
lnHConn = InternetConnect(lnHNet, BLOGSITE, 0, ;
"", "", ;
INTERNET_SERVICE_HTTP, 0, 0)
*!* Once we're connected, we can decide which file we're
*!* interested in asking for.
DECLARE integer HttpOpenRequest IN wininet.dll ;
integer hConnect, ;
string lpszVerb, ;
string lpszObjectName, ;
string lpszVersion, ;
string lpszReferer, ;
string @ lpszAcceptTypes, ;
integer dwFlags, ;
integer dwContext
lcType = ""
lnHRequest = HttpOpenRequest(lnHConn, ;
"GET", RSSFEED, "HTTP/1.1", ;
"", @lcType, 0, 0)
*!* Once everything's set up, actually send the request
*!* to the site, along with any additional headers we
*!* need.
DECLARE integer HttpSendRequest IN wininet.dll ;
integer hRequest, ;
string lpszHeaders, ;
integer dwHeadersLength, ;
integer lpOptional, ;
integer dwOptionalLength
lcModified = "If-Modified-Since:" + HTTPDate(DATE() - 1)
llSentRequest = HttpSendRequest(lnHRequest, ;
lcModified, LEN(lcModified), ;
0, 0)
*!* Once the query's been sent, there's certain info we can
*!* check on, without actually bringing the file back.
DECLARE integer HttpQueryInfo IN wininet.dll ;
integer hRequest, ;
integer dwInfoLevel, ;
string @ lpvBuffer, ;
integer @ lpdwBufferLength, ;
integer lpdwIndex
#DEFINE HTTP_QUERY_STATUS_CODE 19
#DEFINE HTTP_QUERY_CONTENT_LENGTH 5
*!* First, we can check the status code, to see what the
*!* server did with our request.
lcBuffer = SPACE(3)
lnBufferSize = 3
llQuery = HttpQueryInfo(lnHRequest, HTTP_QUERY_STATUS_CODE, ;
@lcBuffer, @lnBufferSize, 0)
*!* If there was no change, only the status code is returned,
*!* with no file.
IF lcBuffer = "304"
MESSAGEBOX("Not updated!")
ELSE
*!* Otherwise, we need to find out how large the return will be.
lcBuffer = SPACE(32)
lnBufferSize = 32
llQuery = HttpQueryInfo(lnHRequest, HTTP_QUERY_CONTENT_LENGTH, ;
@lcBuffer, @lnBufferSize, 0)
*!* Given that information, we know how big a buffer to prepare
*!* for the data when we ask for it. Buffer Overruns Are Evil.
DECLARE integer InternetReadFile IN wininet.dll ;
integer hFile, ;
string @ lpBuffer, ;
integer dwNumberOfBytesToRead, ;
integer @ lpdwNumberOfBytesRead
lnRSSSize = VAL(lcBuffer)
lcRSS = SPACE(lnRSSSize)
lnActualSize = -1
llRead = InternetReadFile(lnHRequest, ;
@lcRSS, lnRSSSize, @lnActualSize)
*!* Now we can show what we brought back. This is the
*!* quick-and-dirty way. :-)
STRTOFILE(lcRSS, "MyBlog.RSS")
MODIFY FILE MyBlog.RSS
ERASE MyBlog.RSS
ENDIF
*!* Now we clean up.
DECLARE integer InternetCloseHandle IN wininet.dll ;
integer hInternet
InternetCloseHandle(lnHRequest)
InternetCloseHandle(lnHConn)
InternetCloseHandle(lnHNet)
*!* This function parses the date into the format expected
*!* in an HTTP header, as specified in RFC2616. Conversion
*!* between GMT and your own time zone is left as an exercise
*!* for the reader.
FUNCTION HTTPDate
LPARAMETERS ttWhen
lcDow = LEFT(CDOW(ttWhen), 3)
lcDate = TRANSFORM(DAY(ttWhen))
lcMonth = LEFT(CMONTH(ttWhen), 3)
lcYear = TRANSFORM(YEAR(ttWhen))
lcHour = PADL(HOUR(ttWhen), 2, "0")
lcMinute = PADL(MINUTE(ttWhen), 2, "0")
lcSecond = PADL(SEC(ttWhen), 2, "0")
lcRetVal = lcDow + ", " + ;
lcDate + " " + ;
lcMonth + " " + ;
lcYear + " " + ;
lcHour + ":" + ;
lcMinute + ":" + ;
lcSecond + " GMT"
RETURN lcRetVal
Update: There’s a problem with the call involving lpszAcceptTypes: see my bugfix post for details.