HOWTO: ISAPI Filter which sets Cache-Control based on Content-Type

Someone recently asked for a supported alternative on IIS6 of the unsupported IIS5-specific hack in KB 247389 . I decided to write up a small ISAPI Filter which uses the supported ISAPI Filter API to perform the same logic as the unsupported hack, and its source code is attached below.

The basic issue is that the IIS5-specific hack took advantage of how IIS5 constructs its response as a buffer, so a malformed MIME Type configuration can be used to append additional response headers onto the raw response as the MIME Type is sent by IIS5. 

However, IIS6 constructs its response as a structured HTTP response, so when IIS6 uses the HTTP API to add the malformed MIME Type, HTTP API detects and rejects it with ERROR_INVALID_PARAMETER.

Since the hack from the KB is unsupported and stated to only apply to IIS5, IIS6 behavior is perfectly correct and by-design.

However, one must realize that this does NOT mean that the feature of "using Content-Type to decide when to send Cache-Control" is unsupported on IIS. Only the way in which the KB accomplishes this, by hacking MIMEType configuration, is unsupported. An ISAPI Filter which implements this feature using documented APIs is perfectly supported.

This ISAPI Filter looks for responses that have "image/gif" and "image/jpeg" as Content-Type and then add "Cache-Control: no-cache" for just those responses.

Enjoy. As usual, since you have the source code, you should support yourself. See the following post for common ISAPI Filter installation issues: https://blogs.msdn.com/david.wang/archive/2005/06/21/HOWTO_Diagnose_and_Fix_Common_ISAPI_Filter_Installation_Failures.aspx

//David

 /*++

Module Name: MIMETypeCacheControl.cpp

Abstract:

    ISAPI Filter to add Cache-Control: none header for responses
    that match certain Content-Type.

Author:

    David Wang      Jun 18, 2005

--*/
#include <windows.h>
#include <httpfilt.h>

#define DEFAULT_BUFFER_SIZE         1024
#define MAX_BUFFER_SIZE             4096

DWORD
WINAPI
OnSendResponse(
    IN HTTP_FILTER_CONTEXT *        pfc,
    IN HTTP_FILTER_SEND_RESPONSE *  pSR
);

BOOL
WINAPI
GetFilterVersion(
    HTTP_FILTER_VERSION *           pVer
    )
/*++

Purpose:

    Required entry point for ISAPI filters.  This function
    is called when the server initially loads this DLL.

Arguments:

    pVer - Points to the filter version info structure

Returns:

    TRUE on successful initialization
    FALSE on initialization failure

--*/
{
    pVer->dwFilterVersion = HTTP_FILTER_REVISION;
    lstrcpyn( pVer->lpszFilterDesc, 
              "Filter to add Cache-Control: none header "
              "for responses with certain Content-Type.", 
              SF_MAX_FILTER_DESC_LEN);
    pVer->dwFlags =
        SF_NOTIFY_ORDER_HIGH |
        SF_NOTIFY_SEND_RESPONSE
        ;

    return TRUE;
}

DWORD
WINAPI
HttpFilterProc(
    IN HTTP_FILTER_CONTEXT *        pfc,
    DWORD                           dwNotificationType,
    LPVOID                          pvNotification
    )
/*++

Purpose:

    Required filter notification entry point.  This function is called
    whenever one of the events (as registered in GetFilterVersion) occurs.

Arguments:

    pfc              - A pointer to the filter context for this notification
    NotificationType - The type of notification
    pvNotification   - A pointer to the notification data

Returns:

    One of the following valid filter return codes:
    - SF_STATUS_REQ_FINISHED
    - SF_STATUS_REQ_FINISHED_KEEP_CONN
    - SF_STATUS_REQ_NEXT_NOTIFICATION
    - SF_STATUS_REQ_HANDLED_NOTIFICATION
    - SF_STATUS_REQ_ERROR
    - SF_STATUS_REQ_READ_NEXT

--*/
{
    switch ( dwNotificationType )
    {
    case SF_NOTIFY_SEND_RESPONSE:

        return OnSendResponse( pfc,
                               (HTTP_FILTER_SEND_RESPONSE *) pvNotification );
    }

    return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD
WINAPI
OnSendResponse(
    IN HTTP_FILTER_CONTEXT *        pfc,
    IN HTTP_FILTER_SEND_RESPONSE *  pSR
)
{
    DWORD                           dwRet = SF_STATUS_REQ_NEXT_NOTIFICATION;
    BOOL                            fRet = FALSE;
    CHAR                            pBuf[ DEFAULT_BUFFER_SIZE ];
    CHAR *                          pszBuf = pBuf;
    DWORD                           cbBuf = DEFAULT_BUFFER_SIZE;
    CHAR                            szCacheControl[] = "Cache-Control:";
    CHAR                            szContentType[] = "Content-Type:";
    CHAR                            szPrivate[] = "no-cache";

    SetLastError( NO_ERROR );

    if ( pfc == NULL ||
         pSR == NULL )
    {
        SetLastError( ERROR_INVALID_PARAMETER );
        goto Finished;
    }

    fRet = pSR->GetHeader( pfc, szContentType, pszBuf, &cbBuf );
    if ( fRet == FALSE )
    {
        if ( GetLastError() == ERROR_INVALID_INDEX )
        {
            //
            // Nothing to do
            //
            OutputDebugString( "Nothing to do.\n" );
            SetLastError( NO_ERROR );
            goto Finished;
        }
        else if ( GetLastError() == ERROR_INSUFFICIENT_BUFFER &&
                  cbBuf < MAX_BUFFER_SIZE )
        {
            pszBuf = new CHAR[ cbBuf ];
            if ( pszBuf == NULL )
            {
                SetLastError( ERROR_NOT_ENOUGH_MEMORY );
                goto Finished;
            }

            fRet = pSR->GetHeader( pfc, szContentType, pszBuf, &cbBuf );
            if ( fRet == FALSE )
            {
                goto Finished;
            }

            OutputDebugString( "Found Content-Type header.\n" );
        }
        else
        {
            goto Finished;
        }
    }

    //
    // At this point, pszBuf points to the value of Content-Type header.
    // Figure out if the Content-Type matches and if so, add the
    // Cache-Control header.
    //
    if ( stricmp( pszBuf, "image/jpeg" ) == 0 ||
         stricmp( pszBuf, "image/gif" ) == 0
        )
    {
        OutputDebugString( "Matched Content-Type value.\n" );

        //
        // Replace any Cache-Control header
        //
        fRet = pSR->SetHeader( pfc, szCacheControl, szPrivate );
        if ( fRet == FALSE )
        {
            //
            // Failed to set header for some reason
            // Fail the request
            //
            goto Finished;
        }

        OutputDebugString( "Added Cache-Control:\n" );
    }

    SetLastError( NO_ERROR );

Finished:

    if ( pszBuf != pBuf )
    {
        delete pszBuf;
    }

    if ( GetLastError() != NO_ERROR )
    {
        OutputDebugString( "Error!\n" );
        dwRet = SF_STATUS_REQ_ERROR;
    }

    return dwRet;
}