nextcloud/server · error · Error
Unable to open a new tab for printing
Error message
Unable to open a new tab for printing
What it means
Thrown by print() in apps/twofactor_backupcodes/src/service/PrintService.ts when window.open() returns null, i.e. the browser refused to open the new tab needed to render the backup codes for printing. Browsers return null (rather than throwing) when a popup blocker or embedding policy blocks the call; the service already shows a user-facing error via showError before rethrowing.
Source
Thrown at apps/twofactor_backupcodes/src/service/PrintService.ts:21
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCapabilities } from '@nextcloud/capabilities'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
/**
* Open a new tab and print the given backup codes
*
* @param data - The backup codes to print
*/
export function print(data: string[]): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const name = (getCapabilities() as any).theming.name || 'Nextcloud'
const newTab = window.open('', t('twofactor_backupcodes', '{name} backup codes', { name }))
if (!newTab) {
showError(t('twofactor_backupcodes', 'Unable to open a new tab for printing'))
throw new Error('Unable to open a new tab for printing')
}
const heading = newTab.document.createElement('h1')
heading.textContent = t('twofactor_backupcodes', '{name} backup codes', { name })
const pre = newTab.document.createElement('pre')
for (const code of data) {
const codeLine = newTab.document.createTextNode(code)
pre.appendChild(codeLine)
pre.appendChild(newTab.document.createElement('br'))
}
newTab.document.body.innerHTML = ''
newTab.document.body.appendChild(heading)
newTab.document.body.appendChild(pre)
newTab.print()
newTab.close()
}View on GitHub (pinned to ecdeb153ff)
Solutions
- Invoke print() synchronously inside the click handler — fetch the codes before showing the button, so no await sits between gesture and window.open
- Add a fallback that renders the codes in the current page (hidden iframe or a print-only element) when window.open returns null
- Tell the user to allow popups for this site (the showError message already hints at the failure)
- Offer alternatives that do not need popups: download as file or copy-to-clipboard
Example fix
// before
async function onPrintClick() {
const codes = await fetchCodes() // async gap breaks user gesture -> popup blocked
print(codes)
}
// after
function onPrintClick() {
print(codes) // codes loaded up-front; window.open runs in the gesture
}
// plus iframe fallback:
function printFallback(codes: string[]) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.srcdoc = `<pre>${codes.join('<br>')}</pre>`
iframe.onload = () => { iframe.contentWindow?.print(); iframe.remove() }
document.body.appendChild(iframe)
} Defensive patterns
Strategy: fallback
Try / catch
try {
print(codes)
} catch (error) {
if (error instanceof Error && error.message.includes('Unable to open a new tab')) {
printFallbackViaIframe(codes) // hidden-iframe print needs no popup
} else throw error
} Prevention
- Never put an await between the user's click and window.open — load the data first, then render the button
- Offer popup-free alternatives: download the codes or copy to clipboard
- In embedded webviews, prefer iframe printing over window.open entirely
When it happens
Trigger: window.open('', ...) returning null: a popup blocker extension/native blocker active, the call happening outside a direct user gesture (any await between the click and the call), the page running in an embedded webview (in-app browser) that forbids popups, or strict enterprise browser policies.
Common situations: User clicks 'Print codes' but an async fetch of the codes happens first, breaking the user-gesture chain; mobile in-app browsers (iOS Safari views, embedded Electron) blocking window.open; users with aggressive ad-blockers; kiosk-mode browsers.
AI-assisted analysis of nextcloud/server@ecdeb153ff (2026-08-17).
Data as JSON: /api/errors/fe5e1d1a48ef2372.
Report an issue: GitHub.