diff --git a/src/htmx.js b/src/htmx.js index 485087a62..e072c5526 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -411,7 +411,6 @@ var htmx = (() => { HCON.merge(sourceElement._htmx.boosted, ctx); } ctx.target = this.__resolveTarget(sourceElement, ctx.target); - ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial"; if (ctx.target) { ctx.request.headers["HX-Target"] = this.__buildIdentifier(ctx.target); } @@ -575,6 +574,8 @@ var htmx = (() => { disableElements = this.__disableElements(elt); ctx.fetch ||= window.fetch.bind(window) + // Set HX-Request-Type based on final target/select (after all modifications) + ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial"; if (!this.__trigger(elt, "htmx:before:request", {ctx})) return; let response = await ctx.fetch(ctx.request.action, ctx.request); diff --git a/test/tests/end2end/basic-history.js b/test/tests/end2end/basic-history.js index 09b30d485..4db1d528b 100644 --- a/test/tests/end2end/basic-history.js +++ b/test/tests/end2end/basic-history.js @@ -511,10 +511,56 @@ describe('scroll restoration on history traversal', function() { }); }); +describe('HX-Request-Type header in history restore', function() { + + beforeEach(function() { setupTest(); }); + afterEach(function() { cleanupTest(); }); + + it('sends HX-Request-Type: full for history restore to body', async function() { + mockResponse('GET', '/restore-test', '
restored
', { headers: { 'HX-Reswap': 'none' } }); + + htmx.__restoreHistory({htmx: true}, '/restore-test'); + await forRequest(); + + assert.equal(lastFetch().request.headers['HX-Request-Type'], 'full'); + }); + + it('sends HX-Request-Type: full for history restore with hx-history-elt', async function() { + playground().innerHTML = '

old

'; + htmx.process(playground()); + + mockResponse('GET', '/restore-test', '

new

', { headers: { 'HX-Reswap': 'none' } }); + + htmx.__restoreHistory({htmx: true}, '/restore-test'); + await forRequest(); + + assert.equal(lastFetch().request.headers['HX-Request-Type'], 'full'); + }); + + it('sends HX-History-Restore-Request: true for history restore', async function() { + mockResponse('GET', '/restore-test', '
restored
', { headers: { 'HX-Reswap': 'none' } }); + + htmx.__restoreHistory({htmx: true}, '/restore-test'); + await forRequest(); + + assert.equal(lastFetch().request.headers['HX-History-Restore-Request'], 'true'); + }); + + it('does not send HX-Request header for history restore (so servers return full pages)', async function() { + mockResponse('GET', '/restore-test', '
restored
', { headers: { 'HX-Reswap': 'none' } }); + + htmx.__restoreHistory({htmx: true}, '/restore-test'); + await forRequest(); + + // HX-Request is intentionally omitted so servers return full pages + assert.isUndefined(lastFetch().request.headers['HX-Request']); + }); +}); + describe('history restore edge cases', function() { - beforeEach(() => { setupTest(this.currentTest); }); - afterEach(() => { cleanupTest(); }); + beforeEach(function() { setupTest(); }); + afterEach(function() { cleanupTest(); }); it('a second back aborts the in-flight restore', async function() { this.timeout(5000); diff --git a/test/tests/unit/headers.js b/test/tests/unit/headers.js index 6d675909f..0743d6004 100644 --- a/test/tests/unit/headers.js +++ b/test/tests/unit/headers.js @@ -65,39 +65,45 @@ describe('Request Headers', function() { describe('HX-Request-Type header', function() { it('sets to partial for regular element target', async function() { - createProcessedHTML('
'); + mockResponse('GET', '/test', 'ok'); + createProcessedHTML('
'); let btn = document.querySelector('button'); - let ctx = htmx.__createRequestContext(btn, new Event('click')); - await htmx.__handleTriggerEvent(ctx); - ctx.request.headers['HX-Request-Type'].should.equal('partial'); + btn.click(); + await forRequest(); + lastFetch().request.headers['HX-Request-Type'].should.equal('partial'); }); it('sets to partial when targeting self', async function() { - let btn = createProcessedHTML(''); - let ctx = htmx.__createRequestContext(btn, new Event('click')); - await htmx.__handleTriggerEvent(ctx); - ctx.request.headers['HX-Request-Type'].should.equal('partial'); + mockResponse('GET', '/test', 'ok'); + let btn = createProcessedHTML(''); + btn.click(); + await forRequest(); + lastFetch().request.headers['HX-Request-Type'].should.equal('partial'); }); it('sets to full when targeting body', async function() { - let btn = createProcessedHTML(''); - let ctx = htmx.__createRequestContext(btn, new Event('click')); - await htmx.__handleTriggerEvent(ctx); - ctx.request.headers['HX-Request-Type'].should.equal('full'); + mockResponse('GET', '/test', 'ok'); + let btn = createProcessedHTML(''); + btn.click(); + await forRequest(); + lastFetch().request.headers['HX-Request-Type'].should.equal('full'); }); it('sets to full when hx-select is present', async function() { - let btn = createProcessedHTML(''); - let ctx = htmx.__createRequestContext(btn, new Event('click')); - await htmx.__handleTriggerEvent(ctx); - ctx.request.headers['HX-Request-Type'].should.equal('full'); + mockResponse('GET', '/test', '
ok
'); + createProcessedHTML('
'); + let btn = document.querySelector('button'); + btn.click(); + await forRequest(); + lastFetch().request.headers['HX-Request-Type'].should.equal('full'); }); it('sets to full when hx-select and body target both present', async function() { - let btn = createProcessedHTML(''); - let ctx = htmx.__createRequestContext(btn, new Event('click')); - await htmx.__handleTriggerEvent(ctx); - ctx.request.headers['HX-Request-Type'].should.equal('full'); + mockResponse('GET', '/test', '
ok
'); + let btn = createProcessedHTML(''); + btn.click(); + await forRequest(); + lastFetch().request.headers['HX-Request-Type'].should.equal('full'); }); }); diff --git a/www/src/content/extensions/14-hx-csp.md b/www/src/content/extensions/14-hx-csp.md index 4d30f56a2..35a98e273 100644 --- a/www/src/content/extensions/14-hx-csp.md +++ b/www/src/content/extensions/14-hx-csp.md @@ -119,6 +119,19 @@ The server cannot know the page nonce — it only knows its own per-response non The risk: unlike `