|
This Issue
In this issue we discuss how to use the Last-Modified header and the Expires header in Active Server pages to save network bandwidth and server resources. Along with the If-Modified-Since header there is a rich client server interaction that can benefit the Active Server developer if they program correct. Included is example code and a complete discussion of the three headers.
Acknowledgements
Thanks to Dave Morris for his insight, discussion, and sample code. Without him this article wouldn't have come together.
Next Issue
The If-Modified-Since header is not returned to the server if the page is not cached on the browser. The Last-modified header and the Expires header directly effect proxy caching. In the next Issue we will talk about browser caching, proxy caching, the Cache-control header and how to make it all work together.
In HTTP, the last modified date is one of the header variables returned from the server in the response to the client (browser). The last modified date is optional, not all responses need to send a last modified date. Here is an example of the header:
Last-modified : Mon, 01 Sep 1997 01:03:33 GMT
What is done with the last modified date and how it effects the user's interaction with the web pages depends greatly on the client. Different clients handle the last modified date differently. Setting the last modified date doesn't guarantee a certain user experience.
Static Pages
The Internet Information Server automatically sends back a last modified date with every static page and graphic that it servers. An example of a static page is any page ending in .htm. The server uses the last modified date of the file on the file system. All files have three dates associated with them, the last modified date, the date the file was created and the date the file was last accessed. When you modify a file with an editor and then save the file, the last modified date changes and so does the header that IIS returns.
We mentioned earlier that the clients handling of the last modified date depends on the client. In the case of static files, most clients handle the Last-Modified header the same. Let's take a look at how four of the major browsers (Internet Explorer 3.x, Internet Explorer 4.0, Netscape 3.0, and Netscape 4.0) handle the last modified date for static files. If the page is not in the file cache of the browser, and the user requests a page, the browser will request the page from the server and write the file to the cache. At the same time, the browser will take the last modified date and write it to the cache. Here is an example of the last modified header that might be returned:
Last-modified : Mon, 01 Sep 1997 01:03:33 GMT
You can examine your cache by looking in the correct folder for your operating system.
If you are running Internet Explorer 3.0 on NT look in:
c:\winnt\ Temporary Internet Files.
If you are running Internet Explorer 3.0 on Windows 95 look in:
c:\windows\ Temporary Internet Files
If you refresh the same page, a header is added to the request because the browser knows the last modified date of the file. The added header is call If-Modified-Since and looks like this:
If-Modified-Since: Wed, 03 Sep 1997 17:03:05 GMT; length=2331
If the page being requested is a graphic or is static, then Internet Information Server uses the If-Modified-Since and compares it against the last modified date on the server. If the If-Modified-Since is younger then or equals the last modified date of the file, the server returns "304 Not Modified" and does not return the HTML data or binary graphic information. If the If-Modified-Since is older then the last modified date of the file on the server, the new file is returned with a status of 200, and a new last modified date. If the browser receives a status of "304 Not Modified" from the Internet Information Server, it uses the file in the browser's cache.
Notice that a return of 304 saves bandwidth and resources since the file is not returned in full. At the same time, the client and the server validate that the file cached on the client side is current. Also notice, that there is no way to make the browser force the server to send down the full page again, even if you are refreshing. It is the server's choice to send the whole page or graphic again or return a "304 Not Modified."
With Active Server pages, it is the responsibility of the web developer to create a last modified header and return it to the client. Since the header is optional, there is no problem with not sending it. However, by setting the header, you can improve server and client performance. Here is how to set the last modified header in an Active Server page:
Response.AddHeader "Last-modified","Mon, 01 Sep 1997 01:03:33 GMT"
If you don't set the last modified header, and you reload or refresh the page, both Internet Explorer and Netscape will request the page and display the response. If you do set the last modified header and you reload or refresh the page Internet Explorer will request the page and if the last modified header is not different in the response then it will display the cached page. Netscape on the other hand will display the response page if the last modified date is the same.
Example 1
<%
Response.AddHeader "Last-modified","Mon, 01 Sep 1997 01:03:33 GMT"
%>
<HTML>
<BODY>
Execute Time : <%=Now()%>
</BODY>
</HTML>
Load this page in Internet Explorer and note the Execute Time. Now refresh the page, notice that the execute time does not change even though the page has been run on the server. The request was run on the server but the client decided to use the one from the cache anyway. This is because the last-modified date had not changed and Internet Explorer is using the last modified date from the cache, which has the first execute time. There is nothing you can do short of changing the Active Server Page on the server, or deleting the page from the browser cache to make this page reload on the client.
Now, load the page with a Netscape browser, notice that the page changes with every reload. Netscape uses the response page when the last modified date is the same.
The things that you should take away from example one:
- If the page changes, you must change the last modified date.
- Different clients treat the last modified date differently.
- The page gets requested, and executed on the server no matter what the last modified date is.
Example 2
In this example, we have a SQL Server database with content in it. All the content is in the tblContent table and each row is to be displayed on a separate page. Each page is simply constructed by formatting a text field called Text that is returned from the database. We will also get the last modified date from the tblContent table. SQL Server will not tell you the last modified date of a row, so you must create another column in the table to store the last modified date. Note here that you will have to update the last modified date every time that the text in the row changes. Here is what the table looks like in SQL script:
CREATE TABLE tblContent (
Id int IDENTITY NOT NULL PRIMARY KEY,
Text text NULL,
LastModified datetime NULL DEFAULT GetDate()
)
Now let's create an Active Server page to display the one row from the tblContent table.
<%
' Get the Id from the URL Encoded String
Id = Request("Id");
' Create a SQL Connection
Set Conn = Server.CreateObject("ADODB.Connection")
Conn.Open "Example","sa",""
' Create SQL Statement
sql = "SELECT LastModified, Text FROM tblContent WHERE Id=" & Id
' Execute SQL Statement
Set RS = Conn.Execute(sql)
' Assign result set to variables
strText=RS("Text")
dtModified=RS("LastModified")
' Clean Up
RS.close
ConnClose
' Adjust From PST to GMT
dtModified = DateAdd ("h",7,dtModified)
' Construct Last Modified String
strModifed= WeekDayName(WeekDay(dtModified),TRUE) &_
", " & Day(dtModified) & " " &_
MonthName(Month(dtModified),TRUE) &_
" " & Year(dtModified) &" "& Hour(dtModified) &_
":" & Minute(dtModified) & ":" &_
Second(dtModified) & " GMT"
' Set the Last Modified Header
Response.AddHeader "Last-modified", strModifed
%>
<HTML>
<BODY>
<%=strText%>
</BODY>
</HTML>
Notice that in the SQL query, we asked for LastModified column before the Text column. This is because of a known bug in ADO which erroneously returns a text field unless it is last in the select list.
We also adjust the Last Modified date to be in Greenwich Mean Time. The SQL Server returns the local time, and since 15 Seconds is in Seattle, we have to add 7 hours to Pacific Standard Time to get to Greenwich Mean Time. The header needs to return in Greenwich Mean Time because this is the HTTP standard. You will have to adjust the number of hours you add or subtract based on your time zone. If you are on the other side of the Greenwich then you can cause yourself a potential problem if you do not adjust the time. Browsers behave erratically when you send them a last modified date that is in the future if you don't adjust the time.
We run through a pretty complicated set of functions to get the date in a format that complies with the HTTP standard. Note here that the singular digits such as "1" should be "01" and the formatting doesn't take this into account. The code and browser are not complete according to the standard, however we can see no noticeable difference in how the browser treats the web pages.
This example returns the last modified date of the text. When the last modified date is changed, Internet Explorer requests will display different pages. However, we are still doing just as much work, or more, as if we where just returning the Text by itself with no last modified date. There is just as much server overhead and network traffic. This is because we treat a request that contains an If-Modified-Since header the same as we would if the header didn't exist. The trick is to handle requests with If-Modified-Since headers differently. The next example shows how to do that.
Example 3
In example three, we will take example two and modify it so that it handles the If-Modified-Since header appropriately.
<%
' Get the Id from the URL Encoded String
Id = Request("Id");
' Create a SQL Connection
Set Conn = Server.CreateObject("ADODB.Connection")
Conn.Open "Example","sa",""
' Create SQL Statement
sql = "SELECT LastModified, Text FROM tblContent WHERE Id=" & Id
' Execute SQL Statement
Set RS = Conn.Execute(sql)
' Assign result set to variables
strText=RS("Text")
' Adjust From PST to GMT
dtModified = DateAdd ("h",7,dtModified)
If (Len(Request.ServerVariables("HTTP_IF_MODIFIED_SINCE"))) Then
strIfModifiedSince=Request.ServerVariables("HTTP_IF_MODIFIED_SINCE")
nIndex=InStr(1,strIfModifiedSince,";",vbTextCompare)
If (nIndex>0) Then
strIfModifiedSince=Left(strIfModifiedSince,nIndex-5)
Else
strIfModifiedSince=Left(strIfModifiedSince,Len(strIfModifiedSince)-5)
End If
nIndex=InStr(1,strIfModifiedSince," ",vbTextCompare)
If (nIndex>0) Then
strIfModifiedSince=Right(strIfModifiedSince,Len(strIfModifiedSince)-nIndex)
End If
dtDate=CDate(strIfModifiedSince)
If (Modified<=dtDate) Then
Response.Clear
Response.Status = "304 Not Modified"
Response.End
EndIIf
End If
dtModified=RS("LastModified")
' Clean Up
RS.close
ConnClose
' Construct Last Modified String
strModifed= WeekDayName(WeekDay(dtModified),TRUE) &_
", " & Day(dtModified) & " " &_
MonthName(Month(dtModified),TRUE) &_
" " & Year(dtModified) &" "& Hour(dtModified) &_
":" & Minute(dtModified) & ":" &_
Second(dtModified) & " GMT"
' Set the Last Modified Header
Response.AddHeader "Last-modified", strModifed
%>
<HTML>
<BODY>
<%=strText%>
</BODY>
</HTML>
In example three, we get the If-Modified-Since header and trim it so that we can cast it to a Visual Basic date. Once cast, we use that date to compare it against the date that came from the SQL Server. If they are equal or the SQL Server date is less then the If-Modified-Since header date, then we return "304 Not Modified". This is exactly the same response that the Internet Information Server gives when handling If-Modified-Since header and a static page that hasn't changed. If the If-Modified-Since header doesn't exist or the file that the browser has is older then the text in the SQL Server we server up the whole page with a new last modified date.
Since the If-Modified-Since header date is in Greenwich Mean Time, we have to adjust the SQL Server date before making the comparison.
Notice that the example three saves network bandwidth because in certain instants the user doesn't receive a complete file on every request like other Active Server pages. However, the example doesn't save any server resources because the Active Server page and the SQL Query must execute every time in order for the page to determine if it had been modified or not. However, this approach would save you time and resources if your Active Server page was making more than 1 query.
Summary
It is important to understand that in no instances above did the browser make a request without user intervention. In other words, every request was made by the user asking for the page initially or from the user hitting reload or refresh. The last modified header doesn't effect the browser in such a way that it requests the page again. The Last-Modified header and If-Modified-Since header are used by the server to determine if a status of "304 Not Modified" should be sent. The client does not use them, accept to communicate the last modified date of the cached file back to the server. The Last-Modified header is not a command to the client. However, the expiration date is a command to the client.
The expiration date is used by the browser to know when the page has expired. If the page has expired and the user goes to that page either by typing in a URL into the address box, using the browsers navigation buttons, or through a link the page is requested again from the server. The page does not automatically make another request if you are viewing it when it expires.
Expires: Tue, 09 Sep 1997 15:42:10 GMT
To set the expiration date in Active Server page you can send the number of minutes to the Expires property in the Response object like this:
' The Page Expires in 15 Minutes
Response.Expires = 15
Or you can send the expiration date directly to the ExpiresAbsolute property, like this:
Response.ExpiresAbsolute = "#05 Sep 1997 17:03:05#"
The ExpiresAbsolute date when wrapped in "#" is relative to the server's local time and not Greenwich Mean Time. If you want every viewing of the page to be requested from the server you can expire the page immediately by setting the Expires property to 0, like this:
Response.Expires = 0
It is important to understand that not setting the expiration date is not equivalent to setting Response.Expires=0. Since the expiration header is optional, not setting the expiration date means that there will be no header returned. Setting Response.Expires=0 means that there will be a header return and the header will contain the current time and date.
Example 4
<%
Response.AddHeader "Last-modified","Mon, 01 Sep 1997 01:03:33 GMT"
Response.Expires=1440
%>
<HTML>
<BODY>
Example 4
</BODY>
</HTML>
This example was last modified on 01 Sep 1997 and the browser must check every day to see if the page has changed.
Summary
The Expires header date directly effects when the client requests pages. If we where going to distinguish between the Last-Modified header and the Expires header, it would be to say that the Last-Modified header is for the server and the Expires header is for the client. Working together you can create solutions that save network bandwidth and server resources or create web applications that work like desktop applications.
IIS 4.0
IIS 4.0 give you the unique ability to set the Expires header for every file served from a particular directory. From MMC you can select the directory and set the Expires header either based on elapsed time from when the file is server or to a fixed date. This give you the ability to set the Expires header without making the file dynamic, in other words it doesn't have to be an HTML header in order for you to set the Expires header. Here is how:
From MMC:
- Highlight the Directory where the files are located by clicking on that directory.
- Right click and choose Properties from the Drop down box.
- Choose the HTTP Headers tab and the property page in Figure 1 will appear.
- Check Enable Content Expiration.
- Set the expiration date using the options available.
- Click on OK to Exit.
Figure 1 : The HTTP Headers Property Page
The Examples
If you want you can download the example that are contained in this issue.
091197.zip
|