Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,13 @@ var htmx = (() => {
ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
if (!this.__trigger(elt, "htmx:before:request", {ctx})) return;

let targetWasConnected = ctx.target?.isConnected;
let response = await ctx.fetch(ctx.request.action, ctx.request);

// Ignore responses whose connected target was removed while the request was in flight.
// This must happen before response hooks (such as hx-head) can apply page-level effects.
if (targetWasConnected && !ctx.target.isConnected) return;

ctx.response = {
raw: response,
status: response.status,
Expand Down
19 changes: 19 additions & 0 deletions test/tests/unit/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,25 @@ describe('ajax() unit Tests', function() {
assert.isFalse(errorFired);
});

it('ignores a response when its target is removed while the request is in flight', async function() {
const seq = mockSequentialResponses('GET', '/test', '<title>Old page</title><p>old</p>');
const target = createProcessedHTML('<div id="target"></div>');
document.title = 'Current page';

const promise = htmx.ajax('GET', '/test', {
target,
swap: 'innerHTML'
});

await htmx.timeout(1);
target.remove();
await seq.next();
await promise;

assert.equal(document.title, 'Current page');
assert.equal(target.innerHTML, '');
});

it('sets full URL for cross-origin GET requests with query params', async function() {
let capturedAction;
document.addEventListener('htmx:before:request', function handler(evt) {
Expand Down