Skip to content

Commit d4d1e4c

Browse files
committed
Force an Asset Manager handshake on the draft stack
Each draft image on a page independently triggers its own Signon/Asset Manager OAuth handshake when there's no existing session. With more than one draft image, these handshakes race and clobber each other's CSRF state, so all but (at best) one image fail to load. In this commit, we load a single placeholder image from the draft-assets host, and once its request has settled - whether it succeeds or fails, we only care that the handshake has finished; we then retry every draft image already on the page with a cache-busting query param, so they get a fresh attempt against what should now be a warm session cookie. Added an `asset-manager-session.js` module that is inlined directly into the page via a `<script>` tag, and only on draft-stack pages, rather than shipped as part of the site's main JavaScript bundle. This is so that live pages are completely unaffected and don't load or run any of this code. It still follows GOV.UK's standard `data-module` convention, so it's found and started automatically like any other page module.
1 parent d566cfd commit d4d1e4c

4 files changed

Lines changed: 180 additions & 0 deletions

File tree

app/assets/config/manifest.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//= link components/map/map-test.geojson
1010
//= link components/map/maplibre-gl-csp-worker.js
1111
//= link views/travel-advice.js
12+
//= link asset-manager-session.js
1213

1314
//= link static-error-pages.js
1415

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* Warms up the Asset Manager draft-assets session cookie via a single
2+
* placeholder image request, then retries any draft images already on
3+
* the page once that request has settled (success or failure).
4+
*
5+
* Usage: add `data-module="AssetManagerSession"` and
6+
* `data-placeholder-asset-url="..."` to an element.
7+
*/
8+
9+
/* istanbul ignore next */
10+
window.GOVUK = window.GOVUK || {}
11+
/* istanbul ignore next */
12+
window.GOVUK.Modules = window.GOVUK.Modules || {};
13+
14+
(function (Modules) {
15+
function AssetManagerSession (element) {
16+
this.element = element
17+
}
18+
19+
AssetManagerSession.prototype.init = function () {
20+
window.addEventListener('load', this.warmUpSession.bind(this))
21+
}
22+
23+
AssetManagerSession.prototype.warmUpSession = function () {
24+
const draftAssets = this.draftAssetImages()
25+
// fewer than 2 images means there's no concurrent-request race to fix, so nothing to warm up
26+
if (draftAssets.length < 2) return
27+
28+
const reloadDraftImages = this.reloadDraftImages.bind(this, draftAssets)
29+
const placeholder = new Image()
30+
placeholder.onload = reloadDraftImages
31+
placeholder.onerror = reloadDraftImages
32+
placeholder.src = this.element.getAttribute('data-placeholder-asset-url')
33+
}
34+
35+
AssetManagerSession.prototype.draftAssetImages = function () {
36+
return [...document.images].filter((image) =>
37+
image.src.includes('assets.') // currently, asset urls in draft preview point to their live link: 'assets.xyz' instead of 'draft-assets.xyz'. Created a backlog item for this: https://gov-uk.atlassian.net/browse/WHIT-4008.
38+
)
39+
}
40+
41+
AssetManagerSession.prototype.reloadDraftImages = function (draftAssets) {
42+
draftAssets.forEach((image) => {
43+
// re-setting src tries to fetch the image again, with the hope that a prior success is just served from cache
44+
image.setAttribute('src', image.getAttribute('src'))
45+
})
46+
}
47+
48+
Modules.AssetManagerSession = AssetManagerSession
49+
// Self-start now instead of waiting for GOVUK.modules.start()
50+
// on DOMContentLoaded, so the window 'load' listener is registered as early as possible.
51+
var element = document.querySelector('[data-module="AssetManagerSession"]')
52+
if (element && !element.getAttribute('data-assetmanagersession-module-started')) {
53+
new AssetManagerSession(element).init()
54+
element.setAttribute('data-assetmanagersession-module-started', 'true')
55+
}
56+
})(window.GOVUK.Modules)

app/views/shared/_footer_navigation.html.erb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,22 @@
88
</div>
99
</div>
1010
<% end %>
11+
12+
<% if draft_host? %>
13+
<%
14+
host = if GovukEnvironment.current == "staging"
15+
"staging.publishing.service.gov.uk"
16+
elsif GovukEnvironment.current == "integration"
17+
"integration.publishing.service.gov.uk"
18+
else
19+
"publishing.service.gov.uk"
20+
end
21+
placeholder_asset_url = "https://draft-assets.#{host}/media/5e59279b86650c53b2cefbfe/placeholder.jpg"
22+
%>
23+
<div
24+
data-module="AssetManagerSession"
25+
data-placeholder-asset-url="<%= placeholder_asset_url %>"
26+
class="govuk-visually-hidden">
27+
</div>
28+
<%= javascript_include_tag "asset-manager-session.js", integrity: false %>
29+
<% end %>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
describe('An asset manager session module', function () {
2+
let element, originalImage
3+
4+
const addImage = (src) => {
5+
const img = document.createElement('img')
6+
img.src = src
7+
document.body.appendChild(img)
8+
return img
9+
}
10+
11+
const trackReloads = (image) => {
12+
const reloadedUrls = []
13+
const originalSetAttribute = image.setAttribute.bind(image)
14+
image.setAttribute = (name, value) => {
15+
if (name === 'src') reloadedUrls.push(value)
16+
originalSetAttribute(name, value)
17+
}
18+
return reloadedUrls
19+
}
20+
21+
const stubImage = ({ succeeds }) => {
22+
class FakeImage {
23+
get src () {
24+
return this._src
25+
}
26+
27+
set src (value) {
28+
this._src = value
29+
succeeds
30+
? this.onload && this.onload()
31+
: this.onerror && this.onerror()
32+
}
33+
}
34+
window.Image = FakeImage
35+
}
36+
37+
beforeEach(function () {
38+
originalImage = window.Image
39+
element = document.createElement('div')
40+
element.setAttribute('data-module', 'AssetManagerSession')
41+
element.setAttribute(
42+
'data-placeholder-asset-url',
43+
'https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg'
44+
)
45+
})
46+
47+
afterEach(function () {
48+
window.Image = originalImage
49+
document.querySelectorAll('img').forEach((img) => img.remove())
50+
})
51+
52+
it('does nothing when fewer than two draft images are on the page', function () {
53+
addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
54+
stubImage({ succeeds: true })
55+
56+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
57+
58+
expect(document.images[0].src).toEqual(
59+
'https://draft-assets.test.gov.uk/media/1/one.jpg'
60+
)
61+
})
62+
63+
it('retries all draft images once the placeholder request succeeds', function () {
64+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
65+
const second = addImage(
66+
'https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar'
67+
)
68+
const firstReloads = trackReloads(first)
69+
const secondReloads = trackReloads(second)
70+
stubImage({ succeeds: true })
71+
72+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
73+
74+
expect(firstReloads).toEqual([
75+
'https://draft-assets.test.gov.uk/media/1/one.jpg'
76+
])
77+
expect(secondReloads).toEqual([
78+
'https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar'
79+
])
80+
})
81+
82+
it('also retries all draft images when the placeholder request errors', function () {
83+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
84+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
85+
const firstReloads = trackReloads(first)
86+
stubImage({ succeeds: false })
87+
88+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
89+
90+
expect(firstReloads).toEqual([
91+
'https://draft-assets.test.gov.uk/media/1/one.jpg'
92+
])
93+
})
94+
95+
it('ignores non-asset-manager images when counting/retrying', function () {
96+
const other = addImage('https://static.test.gov.uk/media/1/one.jpg')
97+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
98+
stubImage({ succeeds: true })
99+
100+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
101+
102+
expect(other.src).toEqual('https://static.test.gov.uk/media/1/one.jpg')
103+
})
104+
})

0 commit comments

Comments
 (0)