Skip to content

Commit 28cb7c3

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.
1 parent d566cfd commit 28cb7c3

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
console.log('eventListener added')
22+
}
23+
24+
AssetManagerSession.prototype.warmUpSession = function () {
25+
const draftAssets = this.draftAssetImages()
26+
if (draftAssets.length < 2) return
27+
28+
const retry = this.retryDraftAssets.bind(this, draftAssets)
29+
const placeholder = new Image()
30+
placeholder.onload = retry
31+
placeholder.onerror = retry
32+
placeholder.src = this.element.getAttribute('data-placeholder-asset-url')
33+
console.log('warmup called with', placeholder.src)
34+
}
35+
36+
AssetManagerSession.prototype.draftAssetImages = function () {
37+
return [...document.images].filter((image) =>
38+
image.src.includes('assets.') // currently, asset urls in draft preview point to their live link: 'assets.xyz' instead of 'draft-assets.xyz'
39+
)
40+
}
41+
42+
AssetManagerSession.prototype.retryDraftAssets = function (draftAssets) {
43+
draftAssets.forEach((image) => {
44+
const separator = image.src.includes('?') ? '&' : '?'
45+
image.src = `${image.src}${separator}_asset_manager_retry=${Date.now()}`
46+
})
47+
}
48+
49+
Modules.AssetManagerSession = AssetManagerSession
50+
})(window.GOVUK.Modules)

app/views/shared/_footer_navigation.html.erb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@
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+
hidden>
27+
</div>
28+
<% end %>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 stubImage = ({ succeeds }) => {
12+
class FakeImage {
13+
get src () {
14+
return this._src
15+
}
16+
17+
set src (value) {
18+
this._src = value
19+
succeeds
20+
? this.onload && this.onload()
21+
: this.onerror && this.onerror()
22+
}
23+
}
24+
window.Image = FakeImage
25+
}
26+
27+
beforeEach(function () {
28+
originalImage = window.Image
29+
element = document.createElement('div')
30+
element.setAttribute('data-module', 'AssetManagerSession')
31+
element.setAttribute(
32+
'data-placeholder-asset-url',
33+
'https://draft-assets.test.gov.uk/media/placeholder/placeholder.jpg'
34+
)
35+
})
36+
37+
afterEach(function () {
38+
window.Image = originalImage
39+
document.querySelectorAll('img').forEach((img) => img.remove())
40+
})
41+
42+
it('does nothing when fewer than two draft images are on the page', function () {
43+
addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
44+
stubImage({ succeeds: true })
45+
46+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
47+
48+
expect(document.images[0].src).toEqual(
49+
'https://draft-assets.test.gov.uk/media/1/one.jpg'
50+
)
51+
})
52+
53+
it('retries all draft images once the placeholder request succeeds', function () {
54+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
55+
const second = addImage(
56+
'https://draft-assets.test.gov.uk/media/2/two.jpg?foo=bar'
57+
)
58+
stubImage({ succeeds: true })
59+
60+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
61+
62+
expect(first.src).toContain('one.jpg?_asset_manager_retry=')
63+
expect(second.src).toContain('two.jpg?foo=bar&_asset_manager_retry=')
64+
})
65+
66+
it('also retries all draft images when the placeholder request errors', function () {
67+
const first = addImage('https://draft-assets.test.gov.uk/media/1/one.jpg')
68+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
69+
stubImage({ succeeds: false })
70+
71+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
72+
73+
expect(first.src).toContain('_asset_manager_retry=')
74+
})
75+
76+
it('ignores non-asset-manager images when counting/retrying', function () {
77+
const other = addImage('https://static.test.gov.uk/media/1/one.jpg')
78+
addImage('https://draft-assets.test.gov.uk/media/2/two.jpg')
79+
stubImage({ succeeds: true })
80+
81+
new GOVUK.Modules.AssetManagerSession(element).warmUpSession()
82+
83+
expect(other.src).toEqual('https://static.test.gov.uk/media/1/one.jpg')
84+
})
85+
})

0 commit comments

Comments
 (0)