Last updatedSep 20, 2019

Cacheable app iframes

This page provides guidance for Atlassian Connect app developers on the topic of enabling app iframes to be cached by the browser. By adopting this approach, app iframes will load dramatically faster resulting in a significantly better user experience. The APIs presented in this page is referred to as the cacheable app iframes or more simply cacheable iframes pattern.

Background

Many apps declare dialogs, panels and pages for seamless integration of the app user interface (UI) with the product UI. Traditionally, Atlassian Connect creates an iframe for each of these modules and passes information to the app using query parameters in the iframe URL. Here is a simplified example of this:

1
2
3
<iframe src="https://some-app.com/confirm-dialog?project=FOO&etc">
	<!-- app content here -->
</iframe>

Unfortunately, query parameters reduce the ability for the browser to cache the app's iframe which causes the browser to re-fetch the app's iframe each time it appears within an Atlassian product. Depending on how the app is served, this can be quite slow and places extra load on the app's server. 

Cacheable iframes

To address this performance issue, Atlassian Connect now allows app iframes to be cached by the web browser. Connect achieves this by omitting all the query parameters it would otherwise append to app iframe URLs.

App architectures to benefit

Not all apps will benefit from the cacheable iframes pattern since it is dependent on how the app pages are rendered. App pages that are dynamically rendered by an app server and dependent on query parameters from the product are not particularly cacheable due to the high likelihood of sending unique combinations of query parameters. In contrast, app pages that do not rely on many query parameters are rendered within the web browser are better candidates for browser caching. There are variations of these two extremes, but essentially, app pages must be static with respect to the URL specified in the module descriptor in order to take advantage of the cacheable iframes pattern.

Opting in to cacheable iframes

The cacheable iframes pattern is based on removing all product injected query parameters from app iframe URLs. This means the new pattern is not backward compatible and must be opted into by apps. The pattern must be adopted on a module by module basis since this approach allows app developers to migrate to the new pattern in an iterative manner. The cacheable iframes pattern applies only to modules that result in the rendering of an iframe in the product UI.

The opt in mechanism involves modifying the app descriptor. Each module adopting the pattern must add a "cacheable" declaration as shown in the following example:

1
2
3
4
5
6
7
8
9
10
11
{
  "modules": {
    "dialogs": [
      {
        "key": "confirm-dialog-module-key",
        "cacheable": true,
        "url": "/confirm-dialog",
      }
    ]
  }
}

Ensuring pages are cacheable

In addition to declaring an app module as being cacheable, the corresponding page must also be served such as to allow it to be cached by the browser. A typical way to do this involves responding to server requests with a Cache-Control header. In addition to this, ETags can also be used to prevent the unnecessary transfer of resources.

Specific advice for caching is not provided since it is dependent on the nature of the app architecture. In particular, special consideration needs to be paid to the fact that changes to the app descriptor require a new version in Marketplace.

For static apps, where all dynamic processing occurs in the browser, the content served by the app server is usually minimal and unlikely to change often. For such apps, it may be prudent to make the resource highly cacheable. In such cases, changes to the shell of the app iframe may necessitate a descriptor update so that the URL to the resource changes. The following app descriptor excerpt illustrates how a static resource may be referenced whereby the v=1 query parameter can be update to v=2 if the app iframe shell changes. 

1
2
3
4
"dialogs": [{
  "key": "my-cacheable-dialog",
  "url": "/my-cacheable-dialog?v=1"
}]

For the above case, where changes to the app iframe shell are rare, it may be advisable to cache it for a long time as per the following Node Express snippet:

1
2
3
4
5
app.get('/my-cacheable-dialog', function (req, res) {
  res.setHeader('Cache-Control', 'private, max-age=31557600, s-maxage=31557600');
  res.set('Expires', 'Mon, 27 Dec 2068 03:03:03 GMT');
  res.render('my-cacheable-dialog');
});

Note that when using the Node Express middleware as implied above, you may also have to disable etags as per the following snippet. This is because browsers will not cache forever if there is an e-tag that can be re-evaluated.

1
2
app.disable('etag');
app.set('etag', false);

Initializing the JavaScript API

An important part of the Atlassian Connect client side framework involves the injection of an API into app iframes which allows the app and client side of the product to communicate directly using Javascript. This is only possible if the app iframe loads a small library, all.js, which helps set up this API. Prior to the introduction of the cacheable  iframes pattern, the location of all.js was only provided relative to the tenant base URL. This meant that app pages had to determine the URL of all.js using a scheme that incorporated parameters  'xdm_e' and 'cp'  which were passed to the app in query parameters. These parameters are not passed in to cacheable iframes, so all.js is now available from the following URL:

https://connect-cdn.atl-paas.net/all.js

It should be noted that the above URL actually identifies an "all.js loader script" which in turn loads all.js. This approach allows successive versions of all.js to be published, since the loader script is responsible for loading the correct version of all.js. The loader script and all versions of all.js are cacheable by the browser which means all.js will load quickly.

The loader script loads and evaluates all.js synchronously. This means apps can simply add the script tag for the loader script before other script tags dependent on it.

1
2
<script src="https://connect-cdn.atl-paas.net/all.js" type="text/javascript"></script>
<script src=“/my-app.js”></script>

During development you may also be interested in loading the non minified version of all.js. This can be done by change the loader script from all.js to all-debug.js.

Getting context

Often an app will need to be aware of the "context" in which it is being displayed. In this regard, context refers to information such as the current user, project, issue, etc. Refer to context parameters for more information about context.

Context information format

After declaring the required context as detailed above, the following example illustrates the format of context information available:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "context": {
    "license": {
      "active": true,
    },   
    "jira": {
      "project": {
        "id": "123",
        "key": "FOO"
      },
      "issue": {
        "id": "12345",
        "key": "FOO-1024",
        "issuetype": {
          "id": "789"
        }
      },
    }
  }
}

Retrieving context using AP.context.getToken()

If an app needs to perform operations dependent on context information in its server, then the app must be declared as using JWT security and the context information must be passed to the server using the JWT token (the JWT contains the context information). Put another way, JWT tokens are the only secure way to pass context information from the product UI to the app server.

The JWT server must decode the JWT and retrieve context information from the "context" claim. The format of the data in the context claim is defined in the section above titled "context information format".

The following snippet provides an example of how to use this:

1
2
3
4
5
6
7
AP.context.getToken(function(token) {
  // Ajax call to app server to act upon the context knowing it will not have been tampered with.
  $.ajax(‘/my-app-operation’, {
    data: token,
    method: ‘POST'
  });
});

Token validity period

The token is generated before the iframe is loaded to ensure calls to AP.context.getToken() are fast. The validity period of JWTs retrieved using the AP.context.getToken() method is fifteen minutes. Calls to AP.context.getToken() made when the token has expired may be delayed by a round trip to the product to generate and retrieve a fresh token. Future enhancements may pro-actively refresh tokens to avoid such delays.

Token query string hash

Tokens retrieved by calls to AP.context.getToken() have no query string hash (QSH) claim since the token is not tied to a specific resource. The token validation logic with the app server must therefore accept JWTs without QSH claims. Note that JWTs used to sign iframes will still have QSH claims which must be validated by the app server.

Retrieving context using AP.context.getContext()

There are scenarios where an app's UI needs direct access to context information as opposed to passing the JWT to the server. Such scenarios include directly displaying the context to the user and making REST calls to the product based on the context information. In both of these scenarios, there is no impact of a malicious user tampering with the context information since it is either contained within the browser or handled by the product.

The following snippet provides an example of how to use this:

1
2
3
AP.context.getContext(function(context) {
  alert('The current project key is ' + context.jira.project.key);
});

Prefetching app iframes

There are often cases where one app module will launch another app module. An typical example involve a page opening a dialog to display information or collect data. If the dialog is declared as a cacheable module then it may make sense for the page launching the dialog to prefetch the dialog iframe resource. This will ensure the dialog content appears straight away, even for users who have never opened the dialog previously.

The following HTML snippet illustrates how the prefetching of the dialog content in the above example would be achieved:

1
2
3
4
5
6
7
8
9
10
<html>
  <head>
    <title>My General Page</title>
    <link rel="prefetch" href="/my-prefetched-dialog">
    <script src="https://connect-cdn.atl-paas.net/all.js" async></script>
  </head>
  <body>
     etc
  </body>
</html>

Supported modules

The initial (September 2018) set of modules that can be declared as "cacheable": true is as follows:

  • Web Panels
  • Dialogs
  • Web items where the target is either dialog or inlineDialog
  • Pages
  • Dynamic Content Macros

Client frameworks

These versions of Atlassian's supported client frameworks accept JWTs without the query string hash (qsh) claim:

FAQ

1. Should I modify my app to use the new cacheable app iframes functionality?

The answer to this question is dependent on the architecture and deployment of the app since some apps will see greater performance improvements than others. We would recommend modifying existing modules on a case by case basis and also in an iterative manner. Perhaps start with a single module to gain some experience with the new API. You might like to capture some metrics before and after the migration to help analyse the benefits.

2. Do AP.context.getContext() and AP.context.getToken() work when called from iframes that are not declared as cacheable?

Yes. The cacheable declaration in the module is primarily used as an opt in mechanism allowing Connect to change its behaviour and not add various query parameters to the iframe URL.

3. Should I modify my app to use AP.context instead of using iframe URL context parameters even if I don't declare my iframe to be cacheable?

If your app does all its dynamic processing of query parameters in its server then probably not because using AP.context would require the logic to be shifted from the server to the client (Javascript code). However, if your app is already extracting query parameters in its Javascript code, then it might be a good idea to start using AP.context since this API is clearer than declaring template URLs in your app descriptor and extracting query parameters. The AP.context API also has the benefit that you don't need to modify your app descriptor if you decide to change your app and, for example, start using a new context property. Remember that app descriptor updates need to be accompanied by a new version of your app in Atlassian Marketplace.

4. Why do the JWTs returned by AP.context.getToken() have a longer expiry time than other JWTs?

The expiry time is somewhat arbitrary, but larger values allow them to be easily passed from Jira or Confluence to the browser and then to the app server with a degree of caching or queuing in intermediate steps. Based on this, we felt justified in extending the expiry time of context JWTs to fifteen minutes as opposed to the three minute expiry time of, for example, the JWTs used to sign app iframe URLs.

5. Why don't the JWTs returned by AP.context.getToken() have a QSH?

The JWTs returned by AP.context.getToken() are not associated with any particular URLs so there's also no query strings to associate them with.