Proxy for Optimizely Search & Navigation tracking script

When using Optimizely Search & Navigation (previously Episerver Find) you will automatically get a reference to a client side Javascript injected into your markup.

<script type="text/javascript" src="https://dl.episerver.net/13.4.4.1/epi-util/find.js"></script>

Occationally, you may want to proxy it via your Optimizely website’s backend, caching it or just make it appear as if it comes from your own domain. It may also be a way of avoiding false positives in regards to script tags not using Subresource Integrity (SRI) hashes where you control the script location yourself.

Edit: see article Finding the latest Optimizely Search & Navigation’s client Javascript URL for a simple way of getting the URL to Optimizely’s latest script.

public class ExternalResourceProxyController : Controller
{
  private const string _jsContentType = "application/javascript";

  // Constant value in example to reduce noise.
  private const string _scriptUrl = "https://dl.episerver.net/13.4.4.1/epi-util/find.js";

  private readonly ExternalResourceService _externalResourceService;

  public ExternalResourceProxyController (ExternalResourceService externalResourceService)
  {
    _externalResourceService = externalResourceService ?? throw new ArgumentNullException (nameof(externalResourceService));
  }

  [HttpGet, Route("ClientResources/static/js/find.js")]
  [OutputCache(VaryByHeader = "Host", Duration = 10 * 60, Location = OutputCacheLocation.Any)]
  public async Task<ContentResult> EpiserverFindScript()
  {
    string content = await _externalResourceService .GetContentAsync(_scriptUrl) .ConfigureAwait(false);
    return new ContentResult
      {
        Content = content,
        ContentType = _jsContentType,
      };
    }

The ExternalResourceService is just a simple disposable class making the call to retireve the client side script, then returning it as a string. You may want to include proper error handling here.

public class ExternalResourceService : IDisposable
{
  /// <remarks>
  /// Register as a singleton.
  /// </remarks>
  internal ExternalResourceService() { }

  private HttpClient _httpClient = new HttpClient();

  public async Task<string> GetContentAsync(Uri url)
  {
    try
    {
      using (HttpResponseMessage response = await _httpClient.GetAsync(url).ConfigureAwait(false))
      {
        _ = response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
      }
    }
    catch (Exception ex)
    {
      // ...
    }
  }

  protected virtual void Dispose(bool disposing)
  {
    if (disposing && _httpClient != null)
    {
      _httpClient.Dispose();
      _httpClient = null;
    }
  }

  public void Dispose()
  {
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
  }
}

To make Optimizely Search & Navigation use your new script path, update the modules.config file with a new clientResource called epi.find.trackingScript.

<module>
  <clientResources>
    <add name="epi.find.trackingScript" path="static/js/find.js" resourceType="Script"/>

Note that the web.config’s system.webServer/caching/profiles/add needs to be updated with varyByHeaders="Host" for extensions used in this proxy (like for instance .js).