Skip to content

Commit 2092801

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 2092801

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
/* Warms up the Asset Manager draft-assets session cookie via a single
3+
* placeholder image request, then retries any draft images already on
4+
* the page once that request has settled (success or failure).
5+
*
6+
* Usage: add `data-module="AssetManagerSession"` and
7+
* `data-placeholder-asset-url="..."` to an element.
8+
*/
9+
;(function (Modules) {
10+
function AssetManagerSession (element) {
11+
this.element = element
12+
}
13+
14+
AssetManagerSession.prototype.init = function () {
15+
const draftAssets = this.draftAssetImages()
16+
if (draftAssets.length < 2) return
17+
18+
const retry = this.retryDraftAssets.bind(this, draftAssets)
19+
const placeholder = new Image()
20+
placeholder.onload = retry
21+
placeholder.onerror = retry
22+
placeholder.src = this.element.getAttribute('data-placeholder-asset-url')
23+
}
24+
25+
AssetManagerSession.prototype.draftAssetImages = function () {
26+
return [...document.images].filter((image) =>
27+
image.src.includes('draft-assets.')
28+
)
29+
}
30+
31+
AssetManagerSession.prototype.retryDraftAssets = function (draftAssets) {
32+
draftAssets.forEach((image) => {
33+
const separator = image.src.includes('?') ? '&' : '?'
34+
image.src = `${image.src}${separator}_asset_manager_retry=${Date.now()}`
35+
})
36+
}
37+
38+
Modules.AssetManagerSession = AssetManagerSession
39+
})(window.GOVUK.Modules)

app/views/shared/_footer_navigation.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@
88
</div>
99
</div>
1010
<% end %>
11+
12+
<% if draft_host? %>
13+
<div
14+
data-module="AssetManagerSession"
15+
data-placeholder-asset-url="<%= placeholder_asset_url %>"
16+
hidden>
17+
</div>
18+
<% end %>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
describe('A 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).init()
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).init()
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).init()
72+
73+
expect(first.src).toContain('_asset_manager_retry=')
74+
})
75+
76+
it('ignores non-draft-assets images when counting/retrying', function () {
77+
const live = addImage('https://assets.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).init()
82+
83+
expect(live.src).toEqual('https://assets.test.gov.uk/media/1/one.jpg')
84+
})
85+
})

0 commit comments

Comments
 (0)