Why some ISAPI Filter events trigger multiple times per request

Question:

Hi everyone,

I wrote a ISAPI filter DLL to process text/html content. Whenever there's a request for a .php file, the OnUrlMap gets called 3 times whereas .html or .asp files causes the right behaviour in Filter (i.e. - OnUrlMap gets called once only). How do I get rid off this problem? Below is the excerpt of the code -

DWORD OnSendRawData(...) {
//Process pvRawData....
........
//Writes HTTP HEADER to client
pfc->WriteClient(....);
//Writes BODY
//pfc->WriteClient(...);
}

When the code executes for HTTP HEADER, the function gets suspended and the second request of the .php file calls OnSendRawData. When any Win32 API gets called in the OnSendRawData code, the second request is suspended and the first one is resumed (i.e.- starts writing the BODY).

As both are same actually request, it messed up the private (or content specific) data.

So, How to stop multiple request in OnUrlMap?

Answer:

Actually, the behaviors you describe are all by-design.

There is no guarantee that every event fires at least once per request. There is also no guarantee that every event fires at most once. In fact, the following events can fire multiple times per request:

  • SF_NOTIFY_READ_RAW_DATA
  • SF_NOTIFY_URL_MAP (MFC Wizard: OnUrlMap)
  • SF_NOTIFY_SEND_RESPONSE (MFC Wizard: OnSendResponse)
  • SF_NOTIFY_SEND_RAW_DATA (MFC Wizard: OnSendRawData)

Thus, your assumption that the "right" behavior for OnUrlMap is getting called once per request... is incorrect. You also assume that OnSendRawData gets called once per request and that no user code can trigger multiple calls to it... and that is incorrect.

Details on the Events

This is how the events actually work:

SF_NOTIFY_READ_RAW_DATA happens each time a data packet is read from the client (assuming either IIS6 running in IIS5 Compatibility Mode or prior IIS versions). This happens at least once per request (to read in the headers), and depending on how the headers are sent and the size of the request entity body, additional events can be triggered by IIS buffering up to W3SVC/UploadReadAheadSize or by ISAPIs making ReadClient() calls. Only the first trigger of this event is guaranteed to happen before any response has been sent to the client; any other trigger of this event can obviously happen at any time during request processing.

SF_NOTIFY_URL_MAP happens each time IIS or user-code requests a URL-to-physical mapping of a URL. This happens at least once per request because IIS has to do the URL-to-physical mapping to obtain the physical resource to process. It can also happen for other reasons, such as:

  • Wildcard Application mappings calling HSE_REQ_EXEC_URL - explicit re-execution of the URL from parent to child, so you have one resolution for parent URL and another for child URL.
  • Default Document resolution, which you can treat like "internal HSE_REQ_EXEC_URL" which re-executes from the parent URL of "/" to the child URL of "/resolved.default.document".
  • Retrieval of server variables like PATH_TRANSLATED by an ISAPI (which includes ASP, ASP.Net, PHP, etc).

SF_NOTIFY_SEND_RESPONSE happens each time IIS or user-code sends structured response headers.

  • This normally happens once per request when IIS sends out the response.
  • However, an ISAPI Filter or ISAPI Extension can also trigger it each time it calls something like SF_REQ_SEND_RESPONSE_HEADER or HSE_REQ_SEND_RESPONSE_HEADER.
  • An ISAPI Filter can prevent this event from triggering if it uses WriteClient to generate a response and then finishes the request with SF_STATUS_REQ_FINISHED before anything else triggers structured response headers to be sent.

SF_NOTIFY_SEND_RAW_DATA happens each  time IIS or user-code sends data to the client.

  • This happens at least once per request if a response has been generated and sent.
  • It also triggers each time an ISAPI Filter or ISAPI Extension calls something like WriteClient() as well as other ServerSupportFunctions which write structured data back to the client, like HSE_REQ_SEND_RESPONSE_HEADER, HSE_REQ_TRANSMIT_FILE, etc.

Conclusion

I am not certain whether your Filter has bugs in OnUrlMap or OnSendRawData, but the code snippet you show is definitely wrong. In general, it is a bad idea to call WriteClient() from OnSendRawData because WriteClient() triggers OnSendRawData... so your code has to somehow WriteClient HTTP_HEADER, and when that WriteClient() triggers OnSendRawData, remember to NOT WriteClient HTTP_HEADER... and so on. Yeah, complicated. Good luck.

Since you are trying to process response content, I assume you are trying to buffer the response with OnSendRawData, make the appropriate modifications, and then flush the modifications to the client. There are a couple of approaches:

  1. Buffer every OnSendRawData event into a huge buffer and suppress the data from being sent by OnSendRawData. Then, in either OnLog or OnEndOfRequest, perform the modification and then flush the entire response buffer with WriteClient(), taking care to NOT buffer the OnSendRawData event triggered by this final WriteClient().
  2. Buffer consecutive OnSendRawData events to perform in-flight modifications as necessary, and then immediately flush the changes with the pvRawData of the consecutive OnSendRawData. Final flush is made with WriteClient from OnLog or OnEndOfRequest, taking care to not buffer the OnSendRawData event triggered by this final WriteClient.

The first approach is easy to implement but buffers the entire response, which can be resource-intensive on large responses/downloads. The second approach is more complicated to implement but only buffers the necessary amount of data to perform necessary modifications.

//David