Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: HTTP recovery in the same tab #876

Merged
merged 4 commits into from
May 13, 2020
Merged
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
31 changes: 6 additions & 25 deletions add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru

// Various types of requests are identified once and cached across all browser.webRequest hooks
const requestCacheCfg = { max: 128, maxAge: 1000 * 30 }
const recoveredTabs = new LRU(requestCacheCfg)
const ignoredRequests = new LRU(requestCacheCfg)
const ignore = (id) => ignoredRequests.set(id, true)
const isIgnored = (id) => ignoredRequests.get(id) !== undefined
Expand Down Expand Up @@ -369,8 +368,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
url.hostname = `${url.hostname}.link`
const redirectUrl = url.toString()
log(`onErrorOccurred: attempting to recover from DNS error (${request.error}) using EthDNS for ${request.url} → ${redirectUrl}`, request)
// TODO: update existing tab
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
return updateTabWithURL(request, redirectUrl, browser)
}

// Check if error can be recovered via DNSLink
Expand All @@ -380,7 +378,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (dnslink) {
const redirectUrl = dnslinkResolver.dnslinkAtGateway(request.url, dnslink)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) using dnslink for ${request.url} → ${redirectUrl}`, request)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
return updateTabWithURL(request, redirectUrl, browser)
}
}

Expand All @@ -394,7 +392,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (isRecoverable(request, state, ipfsPathValidator)) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) for ${request.url} → ${redirectUrl}`, request)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
return updateTabWithURL(request, redirectUrl, browser)
}
},

Expand Down Expand Up @@ -424,7 +422,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
if (isRecoverable(request, state, ipfsPathValidator)) {
const redirectUrl = ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onCompleted: attempting to recover from HTTP Error ${request.statusCode} for ${request.url} → ${redirectUrl}`, request)
return createTabWithURL(request, redirectUrl, browser, recoveredTabs)
return updateTabWithURL(request, redirectUrl, browser)
}
}
}
Expand Down Expand Up @@ -608,29 +606,12 @@ function isRecoverableViaEthDNS (request, state) {
// We can't redirect in onErrorOccurred/onCompleted
// Indead, we recover by opening URL in a new tab that replaces the failed one
// TODO: display an user-friendly prompt when the very first recovery is done
async function createTabWithURL (request, redirectUrl, browser, recoveredTabs) {
async function updateTabWithURL (request, redirectUrl, browser) {
// Do nothing if the URL remains the same
if (request.url === redirectUrl) return

const tabKey = redirectUrl
// reuse existing tab, if exists
// (this avoids duplicated tabs - https://github.com/ipfs-shipyard/ipfs-companion/issues/805)
try {
const recoveredId = recoveredTabs.get(tabKey)
const existingTab = recoveredId ? await browser.tabs.get(recoveredId) : undefined
if (existingTab) {
await browser.tabs.update(recoveredId, { active: true })
return
}
} catch (_) {
// tab no longer exist, let's create a new one
}
const failedTab = await browser.tabs.getCurrent()
const openerTabId = failedTab ? failedTab.id : undefined
const newTab = await browser.tabs.create({
return browser.tabs.update({
active: true,
openerTabId,
url: redirectUrl
})
if (newTab) recoveredTabs.set(tabKey, newTab.id)
}
52 changes: 26 additions & 26 deletions test/functional/lib/ipfs-request-gateway-recover.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,47 +49,47 @@ describe('requestHandler.onCompleted:', function () { // HTTP-level errors
it('should do nothing if broken request is for the default subdomain gateway', async function () {
const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.dweb.link/wiki/', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should redirect to default subdomain gateway on broken subdomain gateway request', async function () {
const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.brokenexample.com/wiki/', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.withArgs({ url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with default subdomain gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true }).calledOnce, 'tabs.update should be called with default subdomain gateway URL')
})
it('should do nothing if broken request is a non-IPFS request', async function () {
const request = urlRequestWithStatus('https://wikipedia.org', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if broken request is a local request to 127.0.0.1/ipfs', async function () {
const request = urlRequestWithStatus('http://127.0.0.1:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if broken request is a local request to localhost/ipfs', async function () {
const request = urlRequestWithStatus('http://localhost:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if broken request is a local request to *.ipfs.localhost', async function () {
const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhw.ipfs.localhost:8080/', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if broken request is to the default public gateway', async function () {
const request = urlRequestWithStatus('https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if broken request is not a \'main_frame\' request', async function () {
const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500, 'stylesheet')
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should recover from unreachable third party public gateway by reopening on the public gateway', async function () {
const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.withArgs({ url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with IPFS default public gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true }).calledOnce, 'tabs.update should be called with IPFS default public gateway URL')
})
})

Expand All @@ -101,17 +101,17 @@ describe('requestHandler.onCompleted:', function () { // HTTP-level errors
it('should do nothing on failed subdomain gateway request', async function () {
const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.brokendomain.com/wiki/', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing on broken non-default public gateway IPFS request', async function () {
const request = urlRequestWithStatus('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 500)
await requestHandler.onCompleted(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
})

afterEach(function () {
browser.tabs.create.reset()
browser.tabs.update.reset()
})

after(function () {
Expand Down Expand Up @@ -148,38 +148,38 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
it('should do nothing if failed request is for the default subdomain gateway', async function () {
const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.dweb.link/wiki/', 500)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should redirect to default subdomain gateway on failed subdomain gateway request', async function () {
const request = urlRequestWithStatus('http://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.brokenexample.com/wiki/', 500)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with default subdomain gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: 'https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link/wiki/', active: true }).calledOnce, 'tabs.update should be called with default subdomain gateway URL')
})
it('should do nothing if failed request is a non-IPFS request', async function () {
const request = urlRequestWithNetworkError('https://wikipedia.org')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if failed request is a non-public IPFS request', async function () {
const request = urlRequestWithNetworkError('http://127.0.0.1:8080/ipfs/QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if failed request is to the default public gateway', async function () {
const request = urlRequestWithNetworkError('https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing if failed request is not a \'main_frame\' request', async function () {
const requestType = 'stylesheet'
const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', 'net::ERR_NAME_NOT_RESOLVED', requestType)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should recover from unreachable third party public gateway by reopening on the public gateway', async function () {
const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with IPFS default public gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: 'https://ipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h', active: true }).calledOnce, 'tabs.update should be called with IPFS default public gateway URL')
})
it('should recover from unreachable HTTP server by reopening DNSLink on the active gateway', async function () {
state.dnslinkPolicy = 'best-effort'
Expand All @@ -188,7 +188,7 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
const expectedUrl = 'http://localhost:8080/ipns/en.wikipedia-on-ipfs.org/'
const request = urlRequestWithNetworkError('https://en.wikipedia-on-ipfs.org/')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: expectedUrl, active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with DNSLink on local gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: expectedUrl, active: true }).calledOnce, 'tabs.update should be called with DNSLink on local gateway URL')
dnslinkResolver.clearCache()
})
it('should recover from failed DNS for .eth opening it on EthDNS gateway at .eth.link', async function () {
Expand All @@ -199,7 +199,7 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
const expectedUrl = 'https://almonit.eth.link/'
const request = urlRequestWithNetworkError('https://almonit.eth', dnsFailure)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.withArgs({ url: expectedUrl, active: true, openerTabId: 20 }).calledOnce, 'tabs.create should be called with ENS resource on local gateway URL')
assert.ok(browser.tabs.update.withArgs({ url: expectedUrl, active: true }).calledOnce, 'tabs.update should be called with ENS resource on local gateway URL')
dnslinkResolver.clearCache()
})
})
Expand All @@ -212,19 +212,19 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
it('should do nothing on unreachable third party public gateway', async function () {
const request = urlRequestWithNetworkError('https://nondefaultipfs.io/ipfs/QmYbZgeWE7y8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing on failed subdomain gateway request', async function () {
const request = urlRequestWithStatus('https://QmYzZgeWE7r8HXkH8zbb8J9ddHQvp8LTqm6isL791eo14h.ipfs.brokendomain.com/wiki/', 500)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
})
it('should do nothing on unreachable HTTP server with DNSLink', async function () {
state.dnslinkPolicy = 'best-effort'
dnslinkResolver.setDnslink('en.wikipedia-on-ipfs.org', '/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco')
const request = urlRequestWithNetworkError('https://en.wikipedia-on-ipfs.org')
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
dnslinkResolver.clearCache()
})
it('should do nothing on failed non-default public gateway IPFS request', async function () {
Expand All @@ -234,13 +234,13 @@ describe('requestHandler.onErrorOccurred:', function () { // network errors
const dnsFailure = 'net::ERR_NAME_NOT_RESOLVED' // chrome code
const request = urlRequestWithNetworkError('https://almonit.eth', dnsFailure)
await requestHandler.onErrorOccurred(request)
assert.ok(browser.tabs.create.notCalled, 'tabs.create should not be called')
assert.ok(browser.tabs.update.notCalled, 'tabs.update should not be called')
dnslinkResolver.clearCache()
})
})

afterEach(function () {
browser.tabs.create.reset()
browser.tabs.update.reset()
})

after(function () {
Expand Down