Search code examples
asp.netwebformstinymcegziphttp-caching

TinyMCE, gzip and caching


I am trying to get TinyMCE, gzip and caching to work correctly but are stuck with the browsers not caching requests to the gzip.ashx handler.

My setup:

This is my code (pretty standard):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <script src="/scripts/tinymce/tinymce.gzip.js"></script>
</head>
<body>
    <script>
        tinymce.init({
        selector: 'textarea',
        plugins: 'image link'
       });
    </script>   
    <textarea />
</body>
</html>

First load of the page:

  1. tinymce.gzip.js request TinyMCE Compressor tinymce.gzip.ashx
  2. tinymce.gzip.ashx compresses all the TinyMCE javascript files and produces a compressed file (.gz) like tinymce.gzip-C3F36E9F5715BFD1943ECF340F1AB753.gz

Subsequent load of the page:

  1. tinymce.gzip.ashx checks if the .gz file exists on disk and returns it to the browser

My tinymce.gzip.ashx (full script at the end of the post) looks like the original but with this minor change, as the page diskcache parameter was not passed in the querystring:

..
...
themes = GetParam("themes", "").Split(',');
diskCache = true; //GetParam("diskcache", "") == "true";
isJS = GetParam("js", "") == "true";
..

Anyways, all this works fine but the real problem occurs in the browsers caching of that .gz file. I can never get it to return a HTTP/1.1 304 Not Modified response, so that .gz will not be requested again. All other files are 304'd.

Here is what I have tried:

  1. I have tried to request the gzip directly like http://mysite/scripts/tinymce/tinymce.gzip-C3F36E9F5715BFD1943ECF340F1AB753.gz but I still get 200 OK

  2. Manually setting Response.StatusCode = 304; will just cause the response to be empty and not load tinymce.

  3. Doing <script src="/scripts/tinymce/tinymce.gzip-C3F36E9F5715BFD1943ECF340F1AB753.gz"></script> will return the .gz file but not load TinyMCE

I have spent five hours now on this - any help is appreciated.

Here are some screenshots from IE 11.0.9600.17498, FF 35.01 and Fiddler:

All other files but the .ashx handler is cached

Response headers are always HTTP/1.1 200 OK

Response headers in FF are the same - HTTP/1.1 200 OK

Fiddler

The full tinymce.gzip.ashx handler:

<%@ WebHandler Language="C#" Class="Handler" %>
/**
 * tinymce.gzip.ashx
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 *
 * This file compresses the TinyMCE JavaScript using GZip and
 * enables the browser to do two requests instead of one for each .js file.
 *
 * It's a good idea to use the diskcache option since it reduces the servers workload.
 */

using System;
using System.Web;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

public class Handler : IHttpHandler {

private HttpResponse Response;
private HttpRequest Request;    
private HttpServerUtility Server;

public void ProcessRequest(HttpContext context) {
    this.Response = context.Response;
    this.Request = context.Request;
    this.Server = context.Server;
    this.StreamGzipContents();
}

public bool IsReusable {
    get {
        return false;
    }
}

#region private

private void StreamGzipContents() {
    string cacheKey = "", cacheFile = "", content = "", enc, suffix, cachePath;
    string[] plugins, languages, themes;
    bool diskCache, supportsGzip, isJS, compress, core;
    int i, x, expiresOffset;
    GZipStream gzipStream;
    Encoding encoding = Encoding.GetEncoding("windows-1252");
    byte[] buff;

    // Get input
    plugins = GetParam("plugins", "").Split(',');
    languages = GetParam("languages", "").Split(',');
    themes = GetParam("themes", "").Split(',');
    diskCache = true; //GetParam("diskcache", "") == "true";
    isJS = GetParam("js", "") == "true";
    compress = GetParam("compress", "true") == "true";
    core = GetParam("core", "true") == "true";
    suffix = GetParam("suffix", "min");
    cachePath = Server.MapPath("."); // Cache path, this is where the .gz files will be stored
    expiresOffset = 10; // Cache for 10 days in browser cache

    // Custom extra javascripts to pack
    string[] custom = {/*
        "some custom .js file",
        "some custom .js file"
    */};

    // Set response headers
    Response.ContentType = "text/javascript";
    Response.Charset = "UTF-8";
    Response.Buffer = false;

    // Setup cache
    Response.Cache.SetExpires(DateTime.Now.AddDays(expiresOffset));
    Response.Cache.SetCacheability(HttpCacheability.Public);
    Response.Cache.SetValidUntilExpires(false);

    // Vary by all parameters and some headers
    Response.Cache.VaryByHeaders["Accept-Encoding"] = true;
    Response.Cache.VaryByParams["theme"] = true;
    Response.Cache.VaryByParams["language"] = true;
    Response.Cache.VaryByParams["plugins"] = true;
    Response.Cache.VaryByParams["lang"] = true;
    Response.Cache.VaryByParams["index"] = true;

    // Setup cache info
    if (diskCache) {
        cacheKey = GetParam("plugins", "") + GetParam("languages", "") + GetParam("themes", "");

        for (i = 0; i < custom.Length; i++)
            cacheKey += custom[i];

        cacheKey = MD5(cacheKey);

        if (compress)
            cacheFile = cachePath + "/tinymce.gzip-" + cacheKey + ".gz";
        else
            cacheFile = cachePath + "/tinymce.gzip-" + cacheKey + ".js";
    }

    // Check if it supports gzip
    enc = Regex.Replace("" + Request.Headers["Accept-Encoding"], @"\s+", "").ToLower();
    supportsGzip = enc.IndexOf("gzip") != -1 || Request.Headers["---------------"] != null;
    enc = enc.IndexOf("x-gzip") != -1 ? "x-gzip" : "gzip";

    // Use cached file disk cache
    if (diskCache && supportsGzip && File.Exists(cacheFile)) {
        Response.AppendHeader("Content-Encoding", enc);
        Response.WriteFile(cacheFile);
        return;
    }

    // Add core
    if (core) {
        content += GetFileContents("tinymce." + suffix + ".js");
    }

    // Add core languages
    for (x = 0; x < languages.Length; x++)
        content += GetFileContents("langs/" + languages[x] + ".js");

    // Add themes
    for (i = 0; i < themes.Length; i++) {
        content += GetFileContents("themes/" + themes[i] + "/theme." + suffix + ".js");

        for (x = 0; x < languages.Length; x++)
            content += GetFileContents("themes/" + themes[i] + "/langs/" + languages[x] + ".js");
    }

    // Add plugins
    for (i = 0; i < plugins.Length; i++) {
        content += GetFileContents("plugins/" + plugins[i] + "/plugin." + suffix + ".js");

        for (x = 0; x < languages.Length; x++)
            content += GetFileContents("plugins/" + plugins[i] + "/langs/" + languages[x] + ".js");
    }

    // Add custom files
    for (i = 0; i < custom.Length; i++)
        content += GetFileContents(custom[i]);

    // Generate GZIP'd content
    if (supportsGzip) {
        if (compress)
            Response.AppendHeader("Content-Encoding", enc);

        if (diskCache && cacheKey != "") {
            // Gzip compress
            if (compress) {
                using (Stream fileStream = File.Create(cacheFile)) {
                    gzipStream = new GZipStream(fileStream, CompressionMode.Compress, true);
                    buff = encoding.GetBytes(content.ToCharArray());
                    gzipStream.Write(buff, 0, buff.Length);
                    gzipStream.Close();
                }
            } else {
                using (StreamWriter sw = File.CreateText(cacheFile)) {
                    sw.Write(content);
                }
            }

            // Write to stream
            Response.WriteFile(cacheFile);
        } else {
            gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress, true);
            buff = encoding.GetBytes(content.ToCharArray());
            gzipStream.Write(buff, 0, buff.Length);
            gzipStream.Close();
        }
    } else
        Response.Write(content);
}

private string GetParam(string name, string def) {
    string value = Request.QueryString[name] != null ? "" + Request.QueryString[name] : def;

    return Regex.Replace(value, @"[^0-9a-zA-Z\\-_,]+", "");
}

private string GetFileContents(string path) {
    try {
        string content;

        path = Server.MapPath(path);

        if (!File.Exists(path))
            return "";

        StreamReader sr = new StreamReader(path);
        content = sr.ReadToEnd();
        sr.Close();

        return content;
    } catch (Exception ex) {
        // Ignore any errors
    }

    return "";
}

private string MD5(string str) {
    MD5 md5 = new MD5CryptoServiceProvider();
    byte[] result = md5.ComputeHash(Encoding.ASCII.GetBytes(str));
    str = BitConverter.ToString(result);

    return str.Replace("-", "");
}

#endregion

}


Solution

  • Looks to me like the ashx page is sending cookies for session state - if you have cookies in asp.net, it will not cache - i've been there myself. You can probably exclude that handler from session, and make sure it does not set any cookies.