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
27 changes: 24 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ async function fetchResults(
state.currentQuery = query

const url = new URL(src, window.location.href)
if (url.origin !== window.location.origin) {
Comment thread
anupamme marked this conversation as resolved.
// Cancel and invalidate any pending request so it can't render stale results.
if (state.controller) {
state.controller.abort()
state.controller = null
remoteInput.removeAttribute('loading')
remoteInput.dispatchEvent(new CustomEvent('loadend'))
}
remoteInput.dispatchEvent(
new CustomEvent('remote-input-error', { bubbles: true })
)
return
Comment thread
anupamme marked this conversation as resolved.
}
const params = new URLSearchParams(url.search)
params.append(remoteInput.getAttribute('param') || 'q', query)
url.search = params.toString()
Expand All @@ -111,21 +124,29 @@ async function fetchResults(
remoteInput.setAttribute('loading', '')
}

state.controller = makeAbortController()
const controller = makeAbortController()
state.controller = controller

let response
let html = ''
try {
response = await fetchWithNetworkEvents(remoteInput, url.toString(), {
signal: state.controller.signal,
signal: controller.signal,
credentials: 'same-origin',
mode: 'same-origin',
headers: { accept: 'text/fragment+html' }
})
html = await response.text()
// A newer request (or a rejected src) superseded this one.
if (state.controller !== controller) return
remoteInput.removeAttribute('loading')
state.controller = null
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
if (
error instanceof Error &&
error.name !== 'AbortError' &&
state.controller === controller
) {
remoteInput.removeAttribute('loading')
state.controller = null
}
Expand Down
12 changes: 12 additions & 0 deletions test/karma.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ function reply(request, response, next) {
response.writeHead(500)
response.ok = false
response.end('Server error')
} else if (request.url.startsWith('/redirect')) {
const host = request.headers.host || ''
const other = host.startsWith('localhost') ? '127.0.0.1' : 'localhost'
response.writeHead(302, {
location: `http://${other}:${host.split(':')[1]}/results`
})
response.end()
} else if (request.url.startsWith('/slow')) {
setTimeout(() => {
response.writeHead(200)
response.end('<ol data-src="slow"><li>stale</li></ol>')
}, 200)
} else if (request.url.startsWith('/network-error')) {
request.destroy(new Error())
response.end()
Expand Down
40 changes: 40 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,46 @@ describe('remote-input', function () {
)
})

it('rejects a cross-origin src', async function () {
remoteInput.src = 'https://example.com/results'
const error = once(remoteInput, 'remote-input-error')
changeValue(input, 'test')
await error
assert.equal(results.innerHTML, '')
})

it('does not follow cross-origin redirects', async function () {
remoteInput.src = '/redirect'
const error = once(remoteInput, 'error')
let succeeded = false
remoteInput.addEventListener('remote-input-success', () => {
succeeded = true
})
changeValue(input, 'test')
await error
assert.notOk(succeeded)
assert.equal(results.innerHTML, '')
})

it('cancels a pending request when src becomes cross-origin', async function () {
remoteInput.src = '/slow'
let succeeded = false
remoteInput.addEventListener('remote-input-success', () => {
succeeded = true
})
changeValue(input, 'test')
assert.ok(remoteInput.hasAttribute('loading'))

const error = once(remoteInput, 'remote-input-error')
remoteInput.src = 'https://example.com/results'
await error
assert.notOk(remoteInput.hasAttribute('loading'))

await new Promise((resolve) => setTimeout(resolve, 400))
assert.notOk(succeeded, 'stale response must not emit success')
assert.equal(results.innerHTML, '')
})

it('repects param attribute', async function () {
remoteInput.setAttribute('param', 'robot')
assert.equal(results.innerHTML, '')
Expand Down