JSS SSR proxy performance issues on Azure App Service

  • When running in the Headless mode, a JSS app receives an incoming HTTP request, which is then rewritten and proxied into a Layout Service request to a Sitecore XP server. When running on Azure each outbound connection from proxy to the Sitecore XP Server uses a SNAT port, and without connection reuse enabled, the risk of SNAT port exhaustion is increased. If this is the case, the application might experience one or more of the following symptoms:
    • Slow response times on all or some of the instances in a service plan.
    • Intermittent 5xx or Bad Gateway errors.
    • Timeout error messages.
    • Cannot connect to external endpoints (like SQLDB, Service Fabric, other App services, and so on.)
  • To resolve the issue, connection keep-alive must be implemented:
    1. Add httpAgents.js with the following content to the application:
      const http = require("http");
      const https = require("https");
      const keepAliveConfig = {
                  maxSockets: 200,
                  maxFreeSockets: 20,
                  timeout: 240 * 1000,
                  freeSocketTimeout: 240 * 1000,
      };
      
      const httpAgent = new keepAlive(keepAliveConfig);
      const httpsAgent = new keepAlive.HttpsAgent(keepAliveConfig);
      
      
      module.exports = {
        setUpDefaultAgents: (serverBundle) => {
          http.globalAgent = httpAgent;
                              https.globalAgent = httpsAgent;
                              if (serverBundle.setUpDefaultAgents) {
                                         serverBundle.setUpDefaultAgents(httpAgent, httpsAgent);
                              }
                  },
                  /**
                   * Enable connection pooling. Adds `connection: keep-alive` header
                   * @param {string} url api host
                  */
        getAgent: (url) => {
                              if (!url) {
                                          throw new Error("[KEEP-ALIVE-CONFIG] SITECORE_API_HOST value is required, but was undefined")
                              }
                              if (!url.indexOf("http://")) return httpAgent;
                              if (!url.indexOf("https://")) return httpsAgent;
                              throw new Error(
                                          "[KEEP-ALIVE-CONFIG] Unexpected SITECORE_API_HOST value, expected http:// or https://, but was " +
                                                     url
                              );
        },
      };
      …
    2. Make changes to config.js of the proxy application:
      …
      const NodeCache = require('node-cache');
      const httpAgents = require("./httpAgents");
      …
      const serverBundle = require(bundlePath);
      httpAgents.setUpDefaultAgents(serverBundle);
      …
      proxyOptions: {
          // Enable connection pooling
          agent: httpAgents.getAgent(apiHost),
          // Setting this to false will disable SSL certificate validation
          // when proxying to a SSL Sitecore instance.
          // This is a major security issue, so NEVER EVER set this to false
          // outside local development. Use a real CA-issued certificate.
                              secure: true,
                              headers: {
                                          "accept-encoding": "gzip, deflate"
                              },
                              xfwd: true
                  },
      …
      return fetch(
            `${config.apiHost}/sitecore/api/jss/dictionary/${appName}/${language}?sc_apikey=${
              config.apiKey
            }`,
            {
              headers: {
                connection: "keep-alive",
              },
            }
          )
      
    3. Make changes to the server.js of your application:
      …
      import Helmet from 'react-helmet';
      import axios from "axios";
      import http from "http";
      import https from "https";
      …
      // Setup Http/Https agents for keep-alive. Used in headless-proxy
      export const setUpDefaultAgents = (httpAgent, httpsAgent) => {
        axios.defaults.httpAgent = httpAgent;
        axios.defaults.httpsAgent = httpsAgent;
        http.globalAgent = httpAgent;
        https.globalAgent = httpsAgent;
      };
      
    4. Make changes to the GraphQLClientFactory.js:
      const link = createPersistedQueryLink().concat(
          new BatchHttpLink({
            uri: endpoint,
            credentials: 'include',
            headers: {
              connection: "keep-alive"
            }
          })
      

    Note
    that currently there is a limitation for usage of "proxyOptions.onProxyReq". Using "onProxyReq" with "keep-alive" can cause the server to crash. You can add custom middleware where you can modify the request before proxying to contain the values you wish to proxy:
    server.use((req, res, next) => {
      // set dynamic headers here
      next();
    });

    For more details, refer to the following useful links:

Applies to:

JSS 11.0.0 - 14.0.2

JSS 15.0.0

November 03, 2020
November 03, 2020

Reference number:

437685

Keywords: 

  • JSS