Skip to content

Every error

Every error a @fkn/lib 0.9.49 call can produce has a row on this page, and a few rows list more than one wording of the same error. This page groups the rows by the calls that produce them and says, for each, what happened, what to do and whether a retry can succeed.

handling errors explains how to match a row. It also lists which fields of an error survive the hop out of the broker, the connection your app holds into FKN.

The handler below sorts an error into five branches by the shape it arrives in. The guards overlap, so test them in this order:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
,
const isLocatorDenied: (error: unknown) => boolean
isLocatorDenied
,
const isTerminalError: (error: unknown) => boolean
isTerminalError
} from '@fkn/lib'
import {
const isNotFound: (error: unknown) => boolean

Whether an error from this module means the path is empty, as opposed to unreadable.

isNotFound
,
class StorageLockedError
StorageLockedError
} from '@fkn/lib/cloud/fs'
import {
const E2E_INTEGRITY_MESSAGE: "fkn:e2e-integrity: stored data failed its integrity check"
E2E_INTEGRITY_MESSAGE
,
const E2E_STALE_EPOCH_MESSAGE: "fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
E2E_STALE_EPOCH_MESSAGE
} from '@fkn/lib/messages'
import {
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
const
const handle: (error: unknown) => Promise<null> | Promise<string | Buffer<ArrayBufferLike>> | null
handle
= (
error: unknown
error
: unknown) => {
// absence and locked, a code and a class, both minted in your own realm
if (
function isNotFound(error: unknown): boolean

Whether an error from this module means the path is empty, as opposed to unreadable.

isNotFound
(
error: unknown
error
)) return null
if (
error: unknown
error
instanceof
class StorageLockedError
StorageLockedError
) return
const promptUnlock: () => Promise<null>
promptUnlock
()
// everything else from the broker, by message prefix
const
const message: string
message
=
error: unknown
error
instanceof
var Error: ErrorConstructor
Error
?
error: Error
error
.
Error.message: string
message
: ''
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
(
const E2E_STALE_EPOCH_MESSAGE: "fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
E2E_STALE_EPOCH_MESSAGE
)) throw
error: unknown
error
// never retry, never overwrite
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
(
const E2E_INTEGRITY_MESSAGE: "fkn:e2e-integrity: stored data failed its integrity check"
E2E_INTEGRITY_MESSAGE
)) throw
error: unknown
error
// never overwrite
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
('storage: api unreachable')) return
const keepLocalCopy: () => null
keepLocalCopy
()
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
('FKN: the broker was replaced')) return
const retryOnce: () => Promise<string | Buffer>
retryOnce
()
// locators and frames, by name, through the exported guards
if (
function isLocatorDenied(error: unknown): boolean
isLocatorDenied
(
error: unknown
error
)) return
const explainRefusal: () => null
explainRefusal
()
if (
function isTerminalError(error: unknown): boolean
isTerminalError
(
error: unknown
error
)) return
const giveUp: () => null
giveUp
()
// the consent refusal, its own name, matched by neither guard above
if (
error: unknown
error
instanceof
var Error: ErrorConstructor
Error
&&
error: Error
error
.
Error.name: string
name
=== 'PermissionDeniedError') return
const explainRefusal: () => null
explainRefusal
()
// the broker deadline, a real class, minted in this realm
if (
error: unknown
error
instanceof
class BrokerUnreachableError
BrokerUnreachableError
) return
const waitAndRetry: () => Promise<string | Buffer>
waitAndRetry
()
throw
error: unknown
error
}
const
const catalog: string | Buffer<ArrayBufferLike> | null
catalog
= await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
const fs_d_exports.promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
unlink: (path: import("node:fs").PathLike) => Promise<void>;
rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>;
readdir: (path: import("node:fs").PathLike) => Promise<string[]>;
mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>;
... 4 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
export fs_d_exports.promises
promises
.
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile
('library/catalog.json', 'utf8').
Promise<string | Buffer<ArrayBufferLike>>.catch<string | Buffer<ArrayBufferLike> | null>(onrejected?: ((reason: any) => string | Buffer<ArrayBufferLike> | PromiseLike<string | Buffer<ArrayBufferLike> | null> | null) | null | undefined): Promise<string | Buffer<ArrayBufferLike> | null>

Attaches a callback for only the rejection of the Promise.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of the callback.

catch
(
const handle: (error: unknown) => Promise<null> | Promise<string | Buffer<ArrayBufferLike>> | null
handle
) // the catalog, or null when nothing is stored yet

On a first run nothing is stored yet, so the read resolves to null. The closing throw passes on a PackagesError or a node-style storage code. Both are matched on code, as their sections below show.

The same handler serves every call site. A storage read never reaches the locator, consent or broker-deadline branches. A realm is one JavaScript execution context, such as a window, a worker or a package tenant. handling errors lists which errors cross out of one.

Type any part of a message, a name, a call or an instruction to narrow every table at once:

Each row has an anchor made from the fixed prefix of its message, the part you match on. /errors/#storage-api-unreachable keeps working for as long as that prefix does. When two rows share a prefix, the later row carries an anchor of its own, as Permission denied: <category> on <site> does.

The root fetch, cloud.fetch and extension.fetch produce these errors, and so do the two header-rule calls, extension.setRequestHeaderRule and extension.removeRequestHeaderRule:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
fetch refuses FKN platform domains (<hostname>)Errorfetch, cloud.fetchThe target hostname is a name FKN owns or any dot-anchored subdomain of one, or the address of an FKN node in any spelling. Through @fkn/lib 0.9.46 the page checked only fkn.app, fkn.dev and sdbx.app, and the broker refused the rest; from 0.9.47 the page checks the full list. A trailing dot is stripped first, so fkn.app. and fkn.app%2e are the same name.Point the call at the app's own api. A capability that carries the user's identity is never aimed at a platform origin.No
fetch: refusing to target the extension's own pages or FKN platform domainsErrorextension.fetch, and the root fetch whenever the extension backend is chosenThe url scheme is chrome-extension: or moz-extension:, or the hostname is a platform host. The extension checks this a second time in its content script, so calling the bridge directly does not skip it. From the first extension store release after 0.1.54 it checks every name FKN owns, and checks once more in its own worker.Point the call at the app's own api, as for the row above.No
<call>: refusing to target FKN platform domainsError, or LocatorDeniedError on a locator callcookies.get, setRequestHeaderRule, the category form of permissions.request, extension.attachFrame and attachFrame on the extension with domains, and extension frame.goto, since @fkn/lib 0.9.46A host the call names is a name FKN owns or a subdomain of one, or an FKN node's address; through @fkn/lib 0.9.46 your realm checked only fkn.app, fkn.dev and sdbx.app. For a domain list a header rule is built from (setRequestHeaderRule, the domains of an attach or a goto, and a goto target's host), a parent of one counts too, such as app or .dev, since a rule matches every host under each domain. <call> is the entry, for example cookies.get or frame.goto. Thrown in your realm before anything is asked or armed. From the first store release after 0.1.54 the extension refuses the same again, on the same full list, and also refuses a locator call, frame.fetch included, whose document or target is on one, with the operation as <call>.Drop the FKN host, or the parent domain that reaches one, from the call. No call is ever aimed at a platform host.No
fetch: refusing a redirect onto FKN platform domainsErrorextension.fetch, and the root fetch on the extension path, from the next FKN extension store release onThe fetch followed a redirect whose final url is on an FKN platform host. A set of browser rules blocks such a hop before it is sent, on Chromium and Firefox and in every spelling of the host, so the fetch normally rejects as on a network error instead. This error is the answer should a hop ever pass those rules, and the response body is discarded unread. Raised from the first store release after 0.1.54.Point the request at a url whose redirects stay off FKN hosts.No
fetch: could not install the rule that keeps a redirect off FKN platform domains, so the request was not sentErrorextension.fetch, and the root fetch on the extension path, with redirect other than 'manual' or 'error', from the next FKN extension store release onThe extension installs a set of session rules each time its worker starts, which block its own requests to FKN platform hosts so a redirect hop onto one is never sent. The browser refused to install them, so a fetch that follows redirects is refused before it is sent rather than sent without the rules. Raised from the first store release after 0.1.54.Pass redirect: 'manual' or redirect: 'error' when the call need not follow a redirect; those are sent as before. Otherwise retry later.Yes, once the extension's worker restarts and installs the rule
fetch with credentials needs the FKN extension, which only exists in window realmsErrorroot fetch with credentials: 'include'The realm has no document, so there is no content script to reach and no session to spend.Do the credentialed call on the main thread, or drop to credentials: 'omit' and let the cloud backend serve it.No
The FKN WebExtension is not installed, enabled or not exposed on this page.Errorextension.fetch, root fetch with credentials: 'include', extension.attachFrame, attachFrame, cookies.get, permissions.request, setRequestHeaderRule, removeRequestHeaderRule: everything that goes through the extension bridgeThe extension marker data-fkn-extension did not appear within 1000 ms plus a 150 ms grace after load, and the install prompt did not produce a usable extension. In a worker realm this arrives as a ReferenceError instead, because MutationObserver and document do not exist there.Offer promptInstall(reason) or a link to the store listing, then retry once the user has installed or enabled it.Yes, once the user installs or enables it
The FKN WebExtension is installed but too old for this page: it speaks ABI <abi> and this page needs at least <required>. Updating the extension fixes this.ExtensionOutdatedErrorframe.requestPermissions on the extension, the category form of permissions.request, and the exposure wait every extension call runsThe extension speaks an older ABI than the call needs. Categories need ABI 2, so an extension below it is refused before anything is sent rather than being asked for a row it cannot draw.Read readExtensionHandshake() first and ask for a category only at abi >= 2 (the handshake carries abi on every status but 'absent'), or catch it and fall back to the per-key { key, scope } form, which every version answers. It is thrown in your own realm, so instanceof works.Yes, once the user updates the extension
fetch: refusing to forge request header(s): <names>Errorextension.fetchThe init.headers carried a header outside the forgeable set origin, referer, cookie. <names> is the deduplicated lowercase list.Send only the three forgeable names through that slot. Every other header is a normal request header and goes through untouched.No
FKN cloud.fetch: no proxy is available (the relay directory could not be read, and no fallback origin is configured)Errorcloud.fetch, root fetch on the cloud pathThe ranked relay directory produced no proxy origin, and the published build carries no fallback origin.Retry after a short backoff. The endpoint cache lives 30000 ms and a cooled relay is retried after 60000 ms.Yes
request rule <id> was not issued to this documentErrorremoveRequestHeaderRule(ruleId)The rule id was never issued to this frame, or was issued for a different rule kind.Remove only the ids your own setRequestHeaderRule returned.No
setRequestHeaderRule: refusing to rewrite request header(s): <names>Errorextension.setRequestHeaderRule, from the first extension store release after 0.1.54An operation in requestHeaders names a header other than Origin, Referer and Cookie, the three extension.fetch can forge. Host and :authority are refused with the rest, since either would pick another site on the same address. <names> is the deduplicated lowercase list. Thrown before anything is asked, and checked again in the extension's worker.Rewrite only Origin, Referer or Cookie in a header rule.No
setRequestHeaderRule: requestHeaders must be an array of header operationsErrorextension.setRequestHeaderRule, from the first extension store release after 0.1.54requestHeaders is not an array, which only an untyped call can send. Thrown before anything is asked.Pass an array of { header, operation, value? } operations.No
Whatever new Request(input, init) throws, typically a TypeErrorTypeErrorextension.fetchThe extension path normalises through a real Request first, so a GET with a body, or a ReadableStream body without duplex: 'half' on Chromium, throws before anything is sent. The text is the browser's.Fix the init so the browser accepts it.No
Permission denied: network on <host> (network.fetchCredentialed <scope>)PermissionDeniedError, with grantKey, category, site, hosts, permissionKey and scopeextension.fetch, root fetch with credentials: 'include'The user refused the Network row for that host, or a stored deny covers it. A local target is the same row, with the chip local network.Match error.name === 'PermissionDeniedError'; neither isLocatorDenied nor isTerminalError matches it. See Permissions and consent.Yes, if the user changes their mind
cookies.get: url must be an absolute http(s) urlErrorcookies.getThe url did not parse on its own, or its scheme is neither http nor https. A cookie has a host or it has nothing, and a grant row is keyed by hostname.Pass an absolute http or https url.No
400 {"error": "fkn-proxy-protocol must be http or https"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe target url scheme is neither http nor https.Use an http or https url. Check response.ok and read error from the JSON body.No
400 {"error": "missing fkn-proxy-hostname"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathNo hostname reached the proxy.Pass an absolute url that carries a hostname.No
403 {"error": "proxying FKN platform domains is not allowed"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe proxy keeps its own copy of the platform host list (fkn.app, fkn.dev, dot-anchored). The library and the broker already refuse sdbx.app before the request leaves, so the server list is shorter by one suffix.Point the request at the app's own api; a platform host is never proxied.No
403 {"error": "egress refused (non-public target)"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathEvery resolved address of the target is checked and one was in a private, loopback, link-local, multicast or documentation range. The check runs inside the DNS resolver too, so the dialed address is the validated one.Target a public address. A host on the local network is reachable through the extension backend, which asks the user for network.fetchLocal.No
429 {"error": "rate limit exceeded"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe per-caller request-rate budget for this tier is spent.Wait, then retry; the budget refills.Yes, after a wait
429 {"error": "upstream origin is saturated"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe proxy holds too many concurrent requests to that one upstream origin for this caller.Retry, and cap how many requests the app keeps open against one origin.Yes, immediately in most cases
413 {"error": "request body exceeds POST_MAX_BODY_SIZE"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe request body is larger than the configured cap.Send a smaller body, or split the upload.No
502 {"error": "upstream fetch failed: <e>"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe upstream connection or read failed. <e> is the transport error text.Retry. Read <e> for what the upstream did.Yes

A row marked No error describes a call that resolves. Check response.ok and the error field of the body instead. The details are on fetch().

Node’s net, dgram, http and dns fail with these errors, and so do their cloud aliases:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
FKN WebVPN does not support IPC connectionsErrornet.Socket#connect, net.createConnection, net.Server#listenThe first argument is a string (a unix socket path) or a Node handle. Thrown synchronously by the argument normaliser.Use a port, or a { port, host } object.No
FKN WebVPN does not support file descriptorsErrornew net.Socket({ fd })options.fd is set.Drop fd.No
Socket not connectedErrornet.Socket#_read, #_write (as the write callback's error), #end, #destroy internalsA read, write or teardown ran before connect() armed the socket promise.Call connect() first, or wait for the connect event.No, until connect() is called
Socket is not connectedErrornet.Socket#localAddress, #localPort, #localFamily, #remoteAddress, #remotePort, #remoteFamilyThe endpoints have not landed yet. The getters throw rather than answer undefined.Read them inside the connect handler. socket.address() answers {} instead of throwing.Yes, after connect fires
Method not implemented.Errornet.Server#getConnections(cb)The browser transport has no connection table.Count connection events yourself.No
Cannot set socket option before connectError, emitted as 'error'net.Socket#setKeepAlive, #setNoDelay and friends before connectThe socket promise does not exist yet. Emitted, not thrown.Set options after connect.No
Missing optionsErrornew dgram.Socket()The constructor requires options. The @fkn/dgram shim's type cast hides this, so new Socket() typechecks and throws at run time.Use dgram.createSocket('udp4').No
Socket not boundErrordgram.Socket#close, #connect, #disconnect, #send internalsThe socket was never bound. Thrown synchronously.Call bind() first, or use send(), which binds on its own.No, until bind()
ERR_SOCKET_DGRAM_NOT_CONNECTEDError (the code is the whole message)dgram.Socket#disconnect, #remoteAddress()The socket holds no remote.Call connect(port, address) first.No
EBADFError (the code is the whole message)dgram.Socket#address()No local address yet.Read it inside the listening handler.Yes, after listening
"offset" is outside of buffer bounds, "length" is outside of buffer boundsRangeError, code ERR_BUFFER_OUT_OF_BOUNDSdgram.Socket#send(msg, offset, length, ...)The range form was used with an offset or length outside the payload. Thrown synchronously.Fix the range.No
Invalid multicast address: <address>Error, emitted as 'error'dgram.Socket#addMembership, #dropMembershipThe string parses as neither IPv4 nor IPv6.Pass a literal group address.No
Invalid IPv4 address: <s>, Invalid IPv6 address: <s>Errordgram.Socket#send on the data-port path, #setMulticastInterfaceAn address literal failed to encode into wire bytes.Pass a valid literal.No
Cannot set headers after they are sent to the clientError, code ERR_HTTP_HEADERS_SENThttp OutgoingMessage#setHeaderThe head block already went out.Set headers before the first write.No
Cannot render headers after they are sent to the clientError, code ERR_HTTP_HEADERS_SENThttp ServerResponse#writeHeadwriteHead was called twice, or after the head went out. Node merges instead; this throws.Call writeHead once.No
Cannot remove headers after they are sent to the clientErrorhttp OutgoingMessage#removeHeaderThe head block already went out.Remove headers before the first write.No
Header name must be a valid HTTP token [<name>]TypeError, code ERR_INVALID_HTTP_TOKENhttp OutgoingMessage#setHeaderThe name is not a string or fails the HTTP token pattern.Fix the name.No
Invalid value "undefined" for header "<name>"TypeError, code ERR_HTTP_INVALID_HEADER_VALUEhttp OutgoingMessage#setHeaderThe value is undefined.Pass a value or omit the header.No
Socket is not availableError, passed to the write callbackhttp OutgoingMessage#write, #endThe response is not bound to a socket.Stop writing once the socket is gone.No
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedBrokerUnreachableErrornet.Socket#connect (<what> is an outbound tcp socket), net.Server#listen (a tcp listener), dgram.Socket#bind (a udp socket)No broker connection exists within the deadline: 8000 ms the first time, 1000 ms after any miss. In a worker realm this is the normal answer until the page calls relayWorker.Bridge the worker with relayWorker(worker), or wait and retry. See Connection and lifecycle.Yes, once the broker is up
getaddrinfo ENOTFOUND <hostname>Errornet.Socket#connect, net.Server#listen, dgram.Socket#bind, #connect, #send to a hostnameThe broker's own dns.lookup returned nothing for the name. The code, errno, syscall and hostname fields are set in the broker realm and are not guaranteed to survive the hop.Match the message, not the code. Check the name; dns.lookup(hostname) answers the same question directly.Sometimes, DNS can change
tcp connect to <address>:<port> timed out after 12000msErrornet.Socket#connectThe relay did not acknowledge the connect within 12000 ms.Retry. If the session produced no metadata at all the transport is also marked stalled and the next dial picks another relay.Yes
The upstream OS error text, for example Connection refused (os error 111)Errornet.Socket#connectThe relay dialed the target and the kernel refused or reset. The relay's own I/O error string is truncated for the wire and rethrown verbatim in the app realm, so the text is the operating system's; the example is the Linux wording and is illustrative.Handle it as you would Node's ECONNREFUSED, matching the text: no Node code survives the hop.Depends on the peer
webvpn: egress to a non-public address refusedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bind, #connectThe target resolves to a loopback, private, link-local or otherwise non-public address and private-target filtering is on. A loopback target is paired locally first, so this is only reached when no local listener owns the port.Use a public target, or a local net.Server in the same broker data plane for loopback pairing.No
webvpn: socket capacity reachedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindThe session's per-session socket budget is spent.Close what you no longer need, then retry.Yes, after closing sockets
webvpn: socket control queue saturatedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindThe session's metadata control queue is full.Back off and retry.Yes
webvpn: account session capacity reachedErrorany socket call that opens a new relay sessionThe account already holds the maximum number of relay sessions.Close another tab or device that is using the relay, then retry.Yes, after closing a session elsewhere
webvpn: token not acceptedErrorany socket call that opens a new relay sessionThe rate token presented at session setup was refused.Reconnect the account; a fresh token is minted in the background.Yes
webvpn: broadcast is not available while private-target filtering is enabledErrordgram.Socket#setBroadcast then #send to a broadcast addressBroadcast is off while the private-target filter is on.Use a unicast target.No
WebVPN setup timeout: <what> took over <ms>msErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindA setup step (for example tcp data stream claim) did not complete inside its deadline.Retry.Yes
WebVPN session closedErrorevery socket, on its 'error' event, and dgram.Socket on transport closeThe relay session ended under the socket. The dgram path substitutes the close reason when the transport supplied one.Reopen the socket. dgram only emits this when an 'error' listener is attached, so 'close' is never swallowed.Yes, a new call redials
FKN WebVPN: no relay reachableErrorthe first socket call in a realmEvery ranked relay failed to dial and none produced a more specific error.Retry after a backoff. A failed relay cools for 60000 ms.Yes
relay directory returned <status>, relay directory answered <content type>, relay directory is empty, no relay directory is configuredErrorthe first socket call in a realmThe relay directory fetch failed, answered the wrong content type, listed nothing, or is unset.Retry the first three; the fourth is a build without a directory and needs a rebuild.Yes for the first three
tcp listener closedErrornet.Server, on pending accept claimsThe listener was closed while a connection claim was outstanding.Ignore it during teardown.No, the server is closed
tcp listener already unbound by relay, tcp socket already shut down by relayErrornet.Server#close, net.Socket#end, #destroyThe relay tore the resource down before the local teardown ran.Ignore it during teardown.No
Invalid packet type <type>, expected <expected>Errorany socket callThe relay answered with a packet the client did not expect, which means the two ends disagree on the wire format.Report it. A reload picks up a newer broker.No

A refused connect, bind or listen reports its error on the socket’s error event. The details are on TCP and UDP sockets and HTTP and DNS.

cloud.fs, the hybrid fs and opfs share these errors, including the messages the storage service itself returns:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
storage locked: connect the app again to open encrypted dataStorageLockedError, code FKN_E2E_LOCKEDcloud.fs.readFile, readFileSealed, writeFile, and the same members through promisesThe broker holds no usable key for this app. The call raises the unlock card first and waits; this error arrives only after the card is dismissed, when no window can show it, or when the popup delivered no usable key.Call unlock() or point the user at the FKN card, then retry. The message deliberately does not match /not found/: locked is unreadable, not empty. See Encryption.Yes, after unlock()
storage: no object at that path, or the original sentence (Not found, storage: read failed (404))StorageNotFoundError, code FKN_STORAGE_NOT_FOUNDcloud.fs.readFile, readFileSealed, writeFileThere is no object at the path. Absence arrives worded two different ways: the api refusing to presign a path with no committed row, and a presign that succeeded followed by a 404 on the object. Both re-mint to this one class.Test isNotFound(err) or err.code === STORAGE_NOT_FOUND, never the message. "Nothing here" is safe to overwrite; "I could not tell you" is not.No
storage: api unreachableError (a FKN_API_UNREACHABLE code is set in the data plane and does not reliably survive the hop)every cloud.fs memberThe fetch to the api's GraphQL endpoint threw. Distinct from an answered error on purpose: a caller may relax an obligation on "nobody answered", never on an answered 500.Match message.startsWith('storage: api unreachable'). Keep the local copy and retry later.Yes
storage: not connectedErrorevery cloud.fs memberNo connect token for this scope, so the account is not connected to this site.Call connect() or account.login(). See Account and quota.Yes, after connect()
storage: the signed-in account changed, so this call was not madeStorageAccountChangedError, code FKN_ACCOUNT_CHANGEDevery cloud.fs member that lists, reads, writes or removes, and an fs call that waits on the account, such as pull, adopt() or a readFile this device cannot answerThe account this page was on when the call was made is not the one signed in now, so FKN refused the call, or the library refused it before sending, and nothing was read, written or removed. A connection renewed without a switch raises it too.Test isAccountChanged(err) or err.code === STORAGE_ACCOUNT_CHANGED, read again what the app is acting on, then make the call again.Yes, as a new call under the account signed in now
storage: the FKN broker cannot pin calls to an account yet, so nothing was sentStorageAccountPinUnsupportedError, code FKN_ACCOUNT_PIN_UNSUPPORTEDevery cloud.fs member that lists, reads, writes or removes, and an fs call that waits on the account, such as pull, adopt() or a readFile this device cannot answerThe broker this page reached, or the shared worker behind it, is older than the account pin, so the call was not sent: an unpinned call could land in an account the app did not mean.Keep the local copy and try again later. It stops once fkn.app has updated.Yes, once fkn.app has updated
storage: the broker answered no account pinErrorevery cloud.fs member that lists, reads, writes or removes, and an fs call that waits on the account, such as pull, adopt() or a readFile this device cannot answerThe broker answered the request for the account pin with no pin, so the call was not sent.Try the call again. The next call asks for the pin afresh.Yes
storage: reserved pathErrorcloud.fs reads, writes and deletes on .fkn or .fkn/*That prefix is platform-internal.Use another path.No
storage: read failed (<status>)Errorcloud.fs.readFile, readFileSealedThe presigned object fetch answered a non-2xx other than 404. A 404 becomes StorageNotFoundError instead.Retry.Yes
storage: write failed (<status>)Errorcloud.fs.writeFileThe presigned upload PUT answered a non-2xx.Retry.Yes
storage query failed: <status>Errorevery cloud.fs memberThe GraphQL response was not ok, or carried no data, and no errors array explained it.Retry.Yes
Invalid pathErrorcloud.fs reads, writes, deletes, renameThe path is empty, longer than 1024 characters, has more than 64 segments, holds a control character, or has any empty, . or .. segment. A leading slash produces an empty first segment, so /library/catalog.json is refused.Use a relative path such as library/catalog.json.No
Storage quota exceededErrorcloud.fs.writeFileThe account's byte limit would be passed. Checked at presign and again at commit.Delete something or upgrade. cloud.fs.quota() reports the headroom.No, until space is freed
Object limit exceededErrorcloud.fs.writeFileThe account holds the maximum number of objects and this write would add one more.Delete an object first.No
Object too largeErrorcloud.fs.writeFileThe declared size, or the uploaded object's real size, is over the per-object cap.Split the file.No
Concurrent update, retryErrorcloud.fs.writeFile, unlinkAnother writer moved the row between the presign and the commit, or between reading and deleting. The compare-and-set refused.Retry the whole write.Yes
Upload not foundErrorcloud.fs.writeFileThe commit named an upload key with no object behind it.Retry the write from the start.Yes
Not signed inErrorevery cloud.fs memberThe bearer resolved to no live session, or its credential generation is stale. The data plane also reports a connect refusal when it sees this exact sentence.Call account.login().Yes, after signing in
Storage is not configuredErrorevery cloud.fs memberThe api instance has storage disabled.Report it; the app cannot change this.No
Not foundErrorcloud.fs.unlink and other members on a .fkn-prefixed or absent rowThe row does not exist, or the path starts with the platform prefix. On readFile and writeFile this is re-minted to StorageNotFoundError; unlink does not run the re-mint, so it arrives as a plain Error and isNotFound answers false.For deletes, match the message, or treat any delete failure as best effort.No
<CODE>: <text>, <syscall> '<path>'Error, with code, path and syscallevery node-style member of fs, opfs, cloud.fsThe usual filesystem conditions, for example ENOENT: no such file or directory, stat 'a/b'. Codes in use: ENOENT, EEXIST, ENOTDIR, EISDIR, ENOTEMPTY, ERR_FS_EISDIR, EBUSY, EINVAL.Branch on err.code exactly as with Node's fs.Depends on the code
storage: <path> exists but could not be read, retry once its scope is availableError, code FKN_E2E_LOCKEDfs.readFileSync, statSync, writeFileSync, renameSync, open, stat, rename on an unhydrated pathThe path is known from a listing but its bytes could not be read into the memory copy, typically because the account is locked. Such a path still answers exists() and still appears in readdir.Unlock, then remount(). Deleting it needs no key.Yes, after unlock()
The "data" argument must be of type string, Buffer, TypedArray, or DataViewTypeErrorfs.writeFile, writeFileSync, appendFile, appendFileSync, and the cloud.fs equivalentsA Blob, an object or anything else was passed as the data.Convert to bytes first. Only a conflict resolver may hand back a Blob.No
opfs: invalid pathErrorfs.pull(path), fs.readFileSealed(path), and every direct opfs memberThe path holds a .. segment, or normalises to nothing. The memory layer never sends .., but pull and readFileSealed pass the raw string.Normalise the path before calling.No
fs: cloud unreachableErrorfs.writeFile, remove, adoptThe cloud state probe answered 'unknown', so nobody could be asked, and there is no local half to queue from. Kept distinct from the row below on purpose.Retry. The probe costs up to 8000 ms the first time and 1000 ms after a timeout.Yes
fs: no storage backend availableErrorfs.writeFile, removeNo OPFS in this realm and the account answered a definite sign-out.Sign in, or accept that this realm has no durable store.Yes, after signing in
fs: this needs a signed-in accountErrorfs.adopt()The cloud half answered 'disconnected'.Call account.login() first.Yes, after signing in

Only a read or a write turns the locked and not-found rows into a StorageLockedError or a StorageNotFoundError. A cloud.fs.unlink failure is not converted, so isNotFound returns false for it. The node-style members carry Node’s own error.code, listed on storage.

Encryption has three messages of its own, exported as constants from @fkn/lib/messages. The storage service answers with four more on a write:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
fkn:e2e-lockedError on the wire, re-minted to StorageLockedError in the app realmcloud.fs reads and writesThe broker holds no usable key for this app. This is the wire form of storage locked, exported as E2E_LOCKED_MESSAGE.Handle StorageLockedError instead; match this prefix only when reading the raw broker error, as a package does.Yes
fkn:e2e-stale-epoch: this file is encrypted under a previous key you resetErrorcloud.fs.readFile, readFileSealedThe object was sealed under a key generation the user has since reset. The old key is gone everywhere. Exported as E2E_STALE_EPOCH_MESSAGE.Do not retry and do not overwrite blindly. The only way back is an encrypted export taken before the reset. See Encryption.No, never
fkn:e2e-integrity: stored data failed its integrity checkErrorcloud.fs.readFile, readFileSealedThe row carries no seal marker, an unknown marker, a scope that disagrees with the bearer, or an envelope whose tag did not verify. Exported as E2E_INTEGRITY_MESSAGE.Never overwrite the path on this error. Surface it and stop.No
Superseded keyErrorcloud.fs.writeFileThe account's key rotated between sealing the bytes and committing them. The commit names the generation it sealed under, so this is caught rather than silently stored.Retry the write; it reseals under the current generation.Yes
This account has no encryption keys yetErrorcloud.fs.writeFileThe account holds no key record, which is what a settled key reset leaves behind.Send the user to the security page to enrol again. cloud.fs.encryption() answers enrolled: false in this state.Yes, after re-enrolling
This account stores sealed objectsErrorcloud.fs.writeFileA write arrived with no seal marker for an account that stores sealed objects.Report it; the broker seals every write, so an app cannot produce this on its own.No
Invalid encryptionErrorcloud.fs.writeFileThe marker on the commit is not one the api recognises.Report it; the marker is set by the broker, not by the app.No

Never retry or overwrite after the stale-epoch row (the file was encrypted under a previous key generation) or the integrity row. The reasons are on encryption.

attachFrame, extension.attachFrame and cloud.attachFrame produce these errors, with an iframe, with window, with blank or with storageState. So do goto, evaluate, addScriptTag, clearCookies, on and the liveness checks that every later call on the Frame runs:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
cloud.attachFrame needs a window realmErrorcloud.attachFrame, and attachFrame when it falls to the cloud backendNo window.Attach on the main thread.No
attachFrame: syncCookies was replaced by cookies. true is cookies: 'persistent', or 'native' for the person's own browser cookies on the extension; false is cookies: 'ephemeral'TypeErrorattachFrame, cloud.attachFrame, extension.attachFrame, with an iframe or a window, since @fkn/lib 0.9.42The options carry a syncCookies key, true, false or undefined alike. The option was renamed and its values split three ways, so the call is refused before anything is attached rather than read under the old meaning.Replace the key with cookies: 'persistent' for the cloud jar true meant there, 'native' for the person's own browser cookies true meant on the extension, 'ephemeral' for false. See which jar, which backend.No
attachFrame: cookies must be 'persistent', 'ephemeral' or 'native', not "<value>"TypeErrorattachFrame, cloud.attachFrame, extension.attachFramecookies was set to something other than the three values.Pass 'persistent', 'ephemeral' or 'native', or leave it out for 'persistent'.No
attachFrame: storageState seeds a fresh jar, so it needs cookies: 'ephemeral'TypeErrorattachFrame, cloud.attachFrame, extension.attachFrame, with an iframe or a window, beside any cookies but 'ephemeral', the default included, since @fkn/lib 0.9.43A seed belongs to a jar of the attachment's own. On 'persistent' a seeded cookie would land in the jar every app of your top-level site shares, and on 'native' in the person's own browser session. Refused before anything is attached.Pass cookies: 'ephemeral' beside storageState, or drop storageState. See seeding a fresh jar.No
attachFrame: storageState.<path> <what is wrong>TypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with storageState and cookies: 'ephemeral', before anything is attachedThe first field of another shape, in the order the state lists them, named by its path: a key the shape does not have, a partitioned cookie's partitionKey and an origin's indexedDB among them; a missing or non-string name, value or domain; a name or value with ; or a control character, or a name with =; a domain that is not a host; a path that does not start with /; an expires that is neither -1 nor a positive number; a sameSite other than 'Strict', 'Lax' or 'None'; a cookie a browser would not store, SameSite=None or a __Secure- name without secure: true, or a __Host- name without secure: true, a host-only domain and path /; an origin that is not an absolute http or https origin; a localStorage item that is not { name, value } strings. A state that is not an object reads attachFrame: storageState must be an object { cookies?, origins? }. The two rows around this one share its prefix.Fix the field the path names, then attach again. A state Playwright saved converts as in seeding a fresh jar.No
attachFrame: storageState.cookies[<i>].expires is milliseconds since the epoch, and <value> reads as a date before 1974; a Playwright storageState gives seconds, so multiply it by 1000TypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with storageState and cookies: 'ephemeral', before anything is attachedA cookie's expires is above 0 and below 100000000000. Playwright's storageState gives seconds and FKN's takes milliseconds, so a state saved by Playwright and passed unchanged meets this: as milliseconds it is a date before 1974, as seconds a date of today. Checked once every field has its shape, so a row above comes first.Multiply each expires by 1000, and keep -1 as it is for a session cookie.No
attachFrame: lockdown runs on the extension, which serves only cookies: 'native'TypeErrorattachFrame, cloud.attachFrame and extension.attachFrame with lockdown: true beside any cookies but 'native', the default includedlockdown is a header policy the extension installs, so it runs on the extension backend only, and the extension serves only the person's own browser cookies.Pass cookies: 'native' beside lockdown, through attachFrame or extension.attachFrame, or drop lockdown.No
cloud.attachFrame: cookies: 'native' needs the FKN browser extension; the cloud render proxy has no browser cookiesTypeErrorcloud.attachFrame with cookies: 'native'The render proxy runs on FKN's own jars, so it has no copy of the person's browser cookies to serve.Call the root attachFrame or extension.attachFrame for 'native', or attach on the cloud with 'persistent' or 'ephemeral'.No
attachFrame: cookies: '<value>' runs on the cloud render proxy; call cloud.attachFrame or the top-level attachFrameExtensionOperationUnsupportedError, operation 'cookies'extension.attachFrame with cookies: 'persistent' or 'ephemeral', so also with no cookies at allThe extension serves only cookies: 'native'. The default is 'persistent', so an extension.attachFrame call written before 0.9.42 meets this. Refused before any exposure wait, with the ABI the extension reported, 0 when none.Pass cookies: 'native' for the person's own browser cookies, or call cloud.attachFrame or the root attachFrame for the other two.No
cloud.attachFrame: the iframe must be connected to the document before attachingErrorcloud.attachFrameThe iframe is not in the document.Append the iframe first.Yes, after appending it
cloud.attachFrame: the iframe's sandbox attribute must include <tokens> for the render proxy to loadErrorcloud.attachFrameA sandbox attribute is present without allow-scripts and allow-same-origin. No sandbox attribute at all is fine.Add the tokens or remove the attribute.No
cloud.attachFrame: the iframe src is not a valid URL: <src>Errorcloud.attachFrameThe src attribute is present but does not resolve to a URL.Fix the src, or leave it empty and use goto.No
cloud.attachFrame: this iframe is already attached; navigate with the Frame returned by that attach or use a fresh iframeErrorcloud.attachFrameThe src already points at the render proxy page.Reuse the Frame from the first attach, or use a fresh iframe.No
attachFrame: refusing to target the extension's own pages or an FKN platform originErrorcloud.attachFrame, extension.attachFrame, attachFrame, with an iframe or a window, and goto on eitherThe target is an extension page or a platform host. Checked page-side for a fast error and again authoritatively in the content script and the render proxy page. For a window on cookies: 'native', from the first extension store release after 0.1.54, the extension's worker checks the window it adopts as well, and closes one found on such a page.Point the frame elsewhere.No
<operation>: could not install the rule that keeps an attached site off FKN platform domains, so nothing was done: <reason>ErrorattachFrame on cookies: 'native', extension.attachFrame and extension frame.goto, from the first extension store release after 0.1.54Before the extension hands an attached frame to your app, and before a goto() navigates it, it installs browser rules in your app's tab that block the attached site's requests to FKN platform hosts. The browser refused them, so the attach or the navigation stops rather than going on without the rules. <operation> is attachFrame or frame.goto, and <reason> is the browser's own message.Retry the call. When it keeps failing, reload the page, which retires the rules the earlier documents in the tab held.Sometimes
cloud.attachFrame: the iframe window is not availableErrorcloud.attachFramecontentWindow is null right after src was set.Retry with a fresh iframe.Yes
cloud.attachFrame: timed out connecting to the render proxy pageErrorcloud.attachFrameThe handshake with the render proxy page did not connect within 20000 ms. On failure the iframe's referrerPolicy, allow and src are restored.Retry.Yes
cloud.attachFrame: the render proxy never became readyErrorcloud.attachFrameThe render proxy page connected but did not report ready within 65000 ms.Retry.Yes
attachFrame: blank must be an object { url }TypeErrorattachFrame and cloud.attachFrame with blankblank was null, a string or another non-object.Pass blank: { url: 'https://example.org/' }.No
attachFrame: blank takes { url }, not "<key>"TypeErrorattachFrame and cloud.attachFrame with blankblank carried a key other than url.Pass url alone.No
attachFrame: blank.url must be an absolute http or https url, not "<value>"TypeErrorattachFrame and cloud.attachFrame with blankblank.url is not a string, does not parse as an absolute url, or has another scheme.Pass the absolute http or https url the empty page is presented at.No
attachFrame: blank needs an iframe with no src; this one has "<src>"TypeErrorattachFrame and cloud.attachFrame with blankThe iframe already has a src other than about:blank, so the page it loads would race the empty one.Attach a fresh iframe with no src.No
attachFrame: blank does not combine with lockdownTypeErrorthe root attachFrame with blank, lockdown and cookies: 'native'; every other combination meets the lockdown or 'native' refusal above firstlockdown is the extension's header policy, and a blank page is served by the cloud render proxy alone.Drop one of them.No
attachFrame: blank runs on the cloud render proxy, which has no browser cookies; pass cookies: 'persistent' or 'ephemeral'TypeErrorthe root attachFrame with blank and cookies: 'native', before any exposure waitOnly the render proxy can present an origin without loading it, and it never runs on the person's own browser cookies.Pass cookies: 'persistent' or 'ephemeral', or leave cookies out.No
attachFrame: a blank page is served by the cloud render proxy, which this build does not configureErrorattachFrame and cloud.attachFrame with blankThis build of @fkn/lib has no render proxy origin configured, so there is nowhere to serve the empty page from.Use a published build of @fkn/lib, which configures one.No
attachFrame: blank does not apply to a windowTypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with window and blankA window opened with no url is already called blank, so the option would mean two things in one call.Attach an iframe for blank, or open the window with window: {} and goto it.No
attachFrame: a blank page is served by the cloud render proxy only; call cloud.attachFrameExtensionOperationUnsupportedError, operation 'blank'extension.attachFrame with blank and cookies: 'native'The extension cannot present an origin without loading it, so no extension build serves a blank page. Refused before anything is dispatched.Call cloud.attachFrame, or the root attachFrame without cookies: 'native'.No
cloud.attachFrame: this FKN page predates blank pages; reload the app to load the current oneLocatorUnsupportedError (terminal)attachFrame and cloud.attachFrame with blank, after the handshakeThe fkn.app page that answered, served from the browser's cache, is older than blank pages. The iframe is put back on about:blank, and nothing was requested from the site.Reload the app, which loads the current page, then attach again.Yes, after a reload
attachFrame: this FKN page predates storageState; reload the app to load the current oneLocatorUnsupportedError (terminal)attachFrame and cloud.attachFrame with storageState, after the handshakeThe fkn.app page that answered, served from the browser's cache, or the render proxy behind it, is older than storageState. The iframe is put back on about:blank, or the window is closed, and nothing was requested from the site.Reload the app, which loads the current page, then attach again.Yes, after a reload
attachFrame: the render proxy did not take the storageState seedTimeoutErrorattachFrame and cloud.attachFrame with storageStateThe render proxy did not confirm the seed within 20000 ms. The iframe is put back on about:blank, or the window is closed, and the page at the attach url was never requested.Attach again.Yes
mirage: the frame host did not answer the localStorage seed, so it may predate seedingErrorattachFrame and cloud.attachFrame with blank and a storageState whose origins carry localStorage itemsThe render proxy's frame host serving the blank page's origin, already mounted when the seed arrives, is older than seeding and did not confirm the localStorage items within 5000 ms. The iframe is put back on about:blank. With a url instead of blank, the same message arrives after the [frame host failed: ](#frame-host-failed) prefix, on the first goto.Reload the app, which loads the current render proxy, then attach again.Yes, after a reload
attachFrame: lockdown needs domains when the frame already has a src; pass domains or load it via gotoErrorextension.attachFrame, attachFrame with lockdown and cookies: 'native'A headers-based lockdown cannot cover a document that is already loading.Pass domains, or attach a blank iframe and goto.No
attachFrame: the iframe must be connected to the document before attachingErrorextension.attachFrame, attachFrame with cookies: 'native'The iframe is not in the document. Checked after the exposure wait, so a missing extension is reported first.Append the iframe first.Yes
frame: no iframe registered for marker="<marker>"Errorextension.attachFrameThe CustomEvent carrying the attach marker never reached the content script.Retry the attach.Yes
The FKN WebExtension is not installed, enabled or not exposed on this page.Errorextension.attachFrame and attachFrame with cookies: 'native'The exposure wait runs before the connected-iframe check, so a missing extension is reported first. ExtensionOutdatedError is the other outcome of the same wait, though the wait itself still accepts every ABI; what raises it is a category ask, which needs ABI 2.Offer promptInstall(reason) or a link to the store listing, as for the fetch row of the same name.Yes, once installed
attachFrame: the browser did not open the window. Call attachFrame directly in a click or key handler, and allow popups for this pageFrameWindowBlockedErrorattachFrame and cloud.attachFrame with window, and attachFrame and extension.attachFrame with window and cookies: 'native' from the first extension store release after 0.1.54window.open returned null, so nothing opened: the call ran without user activation (an await before it, or no click at all), the popup blocker refused it, or the calling frame is sandboxed without allow-popups. A class, minted in your realm.Call attachFrame first in the click or key handler, with no await before it, and ask the user to allow popups for the page. See frames.Yes, from a fresh click
attachFrame: the window refused to attach (<reason>), and loaded nothingFrameWindowRefusedErrorattachFrame and cloud.attachFrame with windowThe window opened and refused, naming reason: 'jar-unreachable' when the cookie jar of the app that opened it could not be reached, 'bad-url' when it would not open the address, 'unknown' for a reason newer than this library. The window stays open and shows why.Branch on error.reason. For 'jar-unreachable', retry from a fresh click, and keep the hidden fkn.app iframe the library adds to your page in the document while the window is open: it is how the window reaches the jar.Sometimes: 'jar-unreachable' from a fresh click, never 'bad-url'
attachFrame: this page has an opaque origin, which a window can never answer, so no window was openedFrameWindowRefusedErrorattachFrame and cloud.attachFrame with windowreason is 'opaque-opener': the page is a file: document or a frame sandboxed without allow-same-origin, so its origin reads 'null' and nothing can address it. Checked before window.open.Open the window from a page with a real origin, or add allow-same-origin to the sandbox.No
attachFrame: a window on cookies: 'native' needs the FKN WebExtension, which is not on this pageExtensionOperationUnsupportedError, operation 'attachWindow', abi 0attachFrame and extension.attachFrame with window and cookies: 'native', since @fkn/lib 0.9.47A window on the person's own browser cookies is a real browser window the extension adopts, and no extension has announced itself on this page. The handshake is read once, with no exposure wait and no install prompt, before anything opens, so the click's activation is left unused.Open the window from the same click with cookies: 'persistent', the default, or 'ephemeral', which run on the cloud backend whatever is installed, or offer promptInstall(reason) for a later click. See a window on the extension.No, but the same click can still open one on another value
extension.attachFrame: the window this page opened was not found; it may have closedErrorattachFrame and extension.attachFrame with window and cookies: 'native', from the first extension store release after 0.1.54The window opened, but the extension's worker never heard of it within 2000 ms, or it was already gone: the person closed it at once, or the worker was installed moments ago and had never run, which drops the browser's report of a new window. The page closes the window before rejecting.Attach a fresh window from a new click. On a fresh install, the next attach is adopted.Yes, from a fresh click
attachFrame: could not install the rule that keeps an attached site off FKN platform domains, so the window was closed: <reason>ErrorattachFrame and extension.attachFrame with window and cookies: 'native', from the first extension store release after 0.1.54Before the extension hands the window to your app it installs browser rules on the window's tab that block the attached site's requests to FKN platform hosts, the same rules an attached iframe gets. The browser refused them, so the extension closed the window rather than hand it over without them. <reason> is the browser's own message.Attach a fresh window from a new click. When it keeps failing, reload the page.Sometimes
attachFrame: the installed FKN WebExtension (ABI <abi>) may predate lockdown, and one that does attaches the frame with its CSP removed rather than locked down. Updating the extension fixes this.ExtensionOperationUnsupportedErrorextension.attachFrame and attachFrame with lockdown: true, since @fkn/lib 0.9.39The installed extension reports an ABI below 1, the first that serves lockdown, or reports none, and one that old drops the option and attaches the frame with its CSP removed. @fkn/lib refuses before anything is attached, with operation 'lockdown' and the ABI the extension reported, 0 when it reports none. A page with no extension at all never reaches this: it takes the install path.Ask the person to update the FKN WebExtension: every build the stores serve carries lockdown, so this is an install that never updated. Attaching without lockdown still works on it, with the CSP removed.Yes, once the extension is updated
cloud.attachFrame: timed out connecting to the windowErrorattachFrame and cloud.attachFrame with windowThe window's channel did not connect within 60000 ms. The window is closed.Attach a fresh window from a new click.Yes, from a fresh click
cloud.attachFrame: the window never became readyErrorattachFrame and cloud.attachFrame with windowThe window connected but its render proxy did not report ready within 65000 ms. The window is closed.Attach a fresh window from a new click.Yes, from a fresh click
cloud.attachFrame: the window closed before it connected. Either the user closed it, or this page's Cross-Origin-Opener-Policy cut its link to the windowLocatorUnsupportedErrorattachFrame and cloud.attachFrame with windowThe window read as closed before its channel connected. A page served with Cross-Origin-Opener-Policy: same-origin loses its window the moment it opens, so this is also what such a page always gets. Terminal.Serve the page with Cross-Origin-Opener-Policy: same-origin-allow-popups or none. Otherwise the user closed it, which is an answer.Only after the header changes, or from a fresh click
cloud.attachFrame: the window went away before it connected; attach a fresh windowLocatorUnsupportedErrorattachFrame and cloud.attachFrame with windowThe window told this page it was going, before its channel connected: it was closed or navigated away. Terminal.Attach a fresh window from a new click.Yes, from a fresh click
cloud.attachFrame: the attached window closed, reloaded or left the page FKN attached; attach a fresh windowLocatorUnsupportedErrorevery call on a WindowFrame once closed has resolved, and an attach whose window closed after connectingThe attachment ended: close() ran, the user closed the window, it reloaded or navigated off the attach page, it lost the app's cookie jar, or the app page went away. The name is set so the dispatch loop stops instead of retrying a dead channel.Attach a fresh window. Read closed to learn the end without a failing call.No, this attachment is over
cloud.attachFrame: the attached window stopped answering; attach a fresh windowLocatorUnsupportedErrorevery call on a WindowFrame after two missed heartbeatsThe window missed two heartbeats in a row, 2000 ms apart, while this page was visible: it crashed or froze. The attachment ended and closed resolved.Attach a fresh window.No, this attachment is over
extension.attachFrame: the attached window closed; attach a fresh windowLocatorUnsupportedErrorevery call on a WindowFrame of cookies: 'native' once closed has resolved, and one in flight when it did, from the first extension store release after 0.1.54The attachment ended: close() ran, anyone closed the window, its page went to an FKN host (a goto that redirected there ends with this), or your page went away. A reload or a navigation in the window does not end it, since the Frame follows the window's page. The name is set so the dispatch loop stops instead of retrying.Attach a fresh window. Read closed to learn the end without a failing call.No, this attachment is over
frame.goto: this window is not attached to this pageErrorgoto on a WindowFrame of cookies: 'native', from the first extension store release after 0.1.54The extension's worker holds no record of this window for the calling page: it closed or reached an FKN host while the goto was on its way, so the attachment is ending.Wait on closed, then attach a fresh window.No, this attachment is over
frame.goto: a window only shows an http or https addressTypeErrorgoto on a WindowFrame of cookies: 'native', from the first extension store release after 0.1.54The goto target resolved to another scheme, such as about:blank or data:. The window does not move.Pass an http or https address.No
attachFrame: pass either iframe or window, not bothTypeErrorattachFrame, cloud.attachFrame, extension.attachFrameThe options carried both iframe and window.Pass one of them.No
attachFrame: lockdown applies to an iframe, never to a windowTypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with windowlockdown: true came with window.Drop lockdown, or attach an iframe on the extension.No
attachFrame: window must be an objectTypeErrorattachFrame, cloud.attachFrame, extension.attachFramewindow was neither undefined nor an object.Pass { url?, width?, height? }, or {} for a blank window.No
attachFrame: window.url must be a stringTypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with windowwindow.url was set to something other than a string.Pass the address as a string.No
attachFrame: window.url is not a valid URL: <url>TypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with windowwindow.url does not resolve against the page's location.href.Pass an absolute url.No
attachFrame: window.url must be an http or https addressTypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with windowThe address resolved to another scheme. A window only ever shows a website.Pass an http or https address, or omit url and goto one later.No
attachFrame: window.<width or height> must be a positive number of CSS pixelsTypeErrorattachFrame, cloud.attachFrame, extension.attachFrame with windowwindow.width or window.height was not a finite number above 0.Pass a positive number, or omit it for the default of 500 by 700.No
frame.goto: waitUntil must be 'load' or 'commit', not "<value>"TypeErrorframe.goto on either backend, since @fkn/lib 0.9.42waitUntil was another value. 'documentstart' is now 'commit', and Playwright's 'domcontentloaded' and 'networkidle' are not served. Thrown before anything is sent, so the frame does not move.Pass 'commit' where you passed 'documentstart', or leave waitUntil out for 'load'.No
frame.goto: pass timeout, in milliseconds; timeoutMs is not an optionTypeErrorframe.gotoThe options carry a timeoutMs key, the name the wire uses. The public option is timeout, as in Playwright.Rename the key to timeout.No
frame.goto: timeout must be a number of millisecondsTypeErrorframe.gototimeout is not a number.Pass a positive number of milliseconds, or leave it out for 30000.No
frame goto: "<url>" is not a url this page can resolveErrorextension frame.gotoThe url does not resolve against the app page's location.href.Pass an absolute url.No
frame load failed for <href>Errorextension frame.goto with the default waitUntil: 'load', on an iframe or a windowThe iframe fired error, or for a window the browser reported the navigation failed. A load another one replaced is not a failure: an abort is ignored, since Firefox reports one before each http load it retries after trying https first.Retry, or check the target.Yes
frame load timed out after <ms>ms for <href>TimeoutErrorextension frame.goto with the default waitUntil: 'load', on an iframe or a windowNo load within the goto's timeout, 30000 ms by default. The extension's message, read back in your realm as a TimeoutError, since @fkn/lib 0.9.42.Raise timeout, or retry.Yes
frame goto: documentstart timed outTimeoutErrorextension frame.goto with waitUntil: 'commit', on an iframe or a windowThe new document's content script did not announce itself within the goto's timeout, or for a window no new document committed in it. A 'commit' goto travels to the extension under the name it has always read there, so its deadline keeps that name in the message.Raise timeout, or retry.Yes
cloud.attachFrame: the render proxy did not answer goto; the frame may have been detached or its page reloadedTimeoutErrorcloud frame.gotoThe render proxy page did not answer within the goto's timeout (30000 ms by default) plus 5000 ms.Check the frame is still attached, then retry with a fresh attach.Sometimes
navigation to <url> did not commit; the frame is still blankErrorcloud frame.goto with the default waitUntil: 'load'The render proxy navigated but no document committed.Retry.Yes
navigation to <url> did not commit a document within <ms>msTimeoutErrorcloud frame.goto with waitUntil: 'commit'No document of the goto's own held the frame within its timeout: the navigation failed, or loaded something that is not a page. The render proxy's message, read back as a TimeoutError.Check the target loads, then retry.Yes
frame host failed: <the frame host's message>Errorcloud frame.goto, and attachFrame with storageState and a url, whose first page is loaded as a gotoThe render proxy's frame host, the document that serves the target origin's pages, failed before it took the navigation, so the goto ends at once rather than at its timeout. Its message follows the prefix: for a host from before seeding, which never answers the localStorage part of a storageState within 5000 ms, mirage: the frame host did not answer the localStorage seed, so it may predate seeding. With blank, whose host is already mounted when the seed arrives, the attach itself rejects with [that message alone](#mirage-the-frame-host-did-not-answer-the-localstorage-seed).Reload the app, which loads the current render proxy, then retry.Yes, after a reload
frame load timed out after <ms>ms for <url>TimeoutErrorcloud frame.goto with the default waitUntil: 'load'The render proxy's own load deadline, the goto's timeout, passed.Raise timeout, or retry.Yes
Permission denied: interaction (embed.open <href>)PermissionDeniedErrorextension frame.gotoThe user refused the navigation. embed.open is severity 0 and audit only, so its row names no site and the message drops the on <host> half.Match error.name === 'PermissionDeniedError' and explain the refusal. See Permissions and consent.Yes
cloud.attachFrame: the attached iframe left the document or was reloaded; attach a fresh iframeLocatorUnsupportedErrorevery locator call and goto on a cloud FrameThe iframe was detached, an ancestor was removed, or contentWindow changed. Only the direct parent is watched; an ancestor removal is caught on the next call. The name is set deliberately so the dispatch loop stops instead of retrying a dead channel.Attach a fresh iframe.No, this attachment is over
frame: this frame no longer holds the document the app attached it toLocatorDeniedError (terminal)every locator call on an extension FrameThe framed document navigated somewhere the app never declared. The message deliberately does not say where it went.goto a declared target again. The policy travels down every frameLocator hop.No
frame: navigate the frame before <operation>LocatorDeniedError (terminal)every gated call on a blank frame, on both backendsThe frame still holds no document, so there is no site to consult and no host to name on a row. An attach with no domains and no goto lands here on the first gated call.goto a target first, or declare domains at attach.No
frame: the document changed under this callLocatorErrorevery gated cloud locator callThe realm that was about to run the operation read its own url and found a different site from the one the middle page picked a grant row for. The check is atomic with the call, so a document that moved mid-call cannot be served under the old row.Let the dispatch loop retry; no handling is needed.Yes, to the deadline
frame: <operation> is only available on an attached frameLocatorDeniedError (terminal)frame.fetch on a frame that is not an attachmentfetch is refused outright on a frame that is not an attachment.Use the Frame returned by attachFrame.No
frame.evaluate: the code did not settle within 30000msTimeoutErrorframe.evaluate on either backendThe code had 30000 ms to settle, counted after any consent card, and did not. Only the wait ends: code that never settles keeps running in the page. A TimeoutError on both backends since @fkn/lib 0.9.42.Return sooner, or split the work into calls that each settle.Yes
The FKN WebExtension does not support "<operation>" (ABI <abi>). Updating the extension may add it.ExtensionOperationUnsupportedErrorextension frame.evaluate, on an extension below ABI 3, with operation 'evaluate'; and attachFrame and extension.attachFrame with window and cookies: 'native', on an extension that does not announce attachWindow (every store build through 0.1.54), with operation 'attachWindow', since @fkn/lib 0.9.47The installed extension does not announce the operation, so it would not run the code or open the window. A window is refused before anything opens, with no wait and no install prompt, so the click's activation is left for your fallback. This is the class's default wording; every other construction passes a message of its own, each with its row on this page.Ask the person to update the extension. For evaluate, attach on the cloud backend, which runs it on every version; for a window, open it from the same click on cookies: 'persistent' or 'ephemeral'.Yes, once the extension is updated
frame.addScriptTag: options must be an object { content }TypeErrorframe.addScriptTag, before anything is sentThe argument was not a plain object.Pass { content: '...' }.No
frame.addScriptTag: "<key>" is not served; it runs content as a classic inline scriptTypeErrorframe.addScriptTag, before anything is sentOne of Playwright's url, path or type was passed. The script is always the content you pass, run as a classic inline script.Pass the source as content, and drop the key.No
frame.addScriptTag: unknown option "<key>"TypeErrorframe.addScriptTag, before anything is sentA key other than content and sourceUrl.Drop the key.No
frame.addScriptTag: content must be a stringTypeErrorframe.addScriptTag, before anything is sentcontent is missing or not a string.Pass the script source as a string.No
frame.addScriptTag: sourceUrl must be a non-empty string with no line breakTypeErrorframe.addScriptTag, before anything is sentsourceUrl is empty, not a string, or holds a line terminator that would end its //# sourceURL comment early.Pass a one-line name, or leave sourceUrl out.No
frame.addScriptTag: the call arrived without an Evaluation grant, so it was not runLocatorDeniedError (terminal)cloud frame.addScriptTagThe render proxy found no proof of an Evaluation grant on the call, which is what an fkn.app page that predates addScriptTag sends. The script did not run.Reload the app, which loads the current page, then call again.Yes, after a reload
frame.addScriptTag: this render proxy predates addScriptTag; reload the appLocatorUnsupportedError (terminal)cloud frame.addScriptTagThe render proxy that answered is older than the operation. The script did not run.Reload the app, then call again.Yes, after a reload
frame.addScriptTag: no answer within 30000ms; the script may or may not have runTimeoutErrorcloud frame.addScriptTagThe render proxy did not answer within 30000 ms, counted after any consent card. Whether the script ran is unknown.Make the script idempotent before calling again, since it may already have run.Yes, with an idempotent script
frame.addScriptTag: an extension frame does not run scripts through mirage yet; attach with cloud.attachFrameExtensionOperationUnsupportedError, operation 'addScriptTag'extension frame.addScriptTagNo extension build serves the operation yet. Refused before anything is dispatched.Attach on the cloud backend for addScriptTag, or run the code with frame.evaluate, which the extension serves.No
frame.clearCookies: options must be an objectTypeErrorframe.clearCookies, before anything is sentThe argument was neither undefined nor a plain object.Pass { name?, domain?, path? }, or nothing to clear every cookie the attachment reaches.No
frame.clearCookies: unknown option "<key>"TypeErrorframe.clearCookies, before anything is sentA key other than name, domain and path.Drop the key.No
frame.clearCookies: <key> is a RegExp, and clearCookies takes strings for name, domain and path for nowTypeErrorframe.clearCookies, before anything is sentPlaywright's clearCookies also takes a RegExp, which FKN refuses for now: a pattern would be tested in the render proxy against cookies the app cannot read, so its running time could say which of them exist. A RegExp from another realm is refused too.Pass the exact string, once per value to remove.No
frame.clearCookies: <key> must be a stringTypeErrorframe.clearCookies, before anything is sentname, domain or path was set to something other than a string or undefined.Pass a string, or leave the option out to match every value.No
frame.clearCookies: <key> must not be empty; leave it out to match every <key>TypeErrorframe.clearCookies, before anything is sentAn empty string. Playwright reads one as no filter and clears every cookie, so a computed empty name would empty the jar; FKN refuses it instead.Leave the option out to match every value, or pass the value you mean.No
frame.clearCookies: this attachment reaches no site yet; attach a url, goto one, or declare it in domainsLocatorDeniedError (terminal)cloud frame.clearCookiesThe attachment has no attach url, no goto target and no domains, so it reaches no site whose cookies it could remove.goto the site, or declare it in domains at attach.No
frame.clearCookies: <domain> is not on a site this attachment reaches; goto it or declare it in domainsLocatorDeniedError (terminal)cloud frame.clearCookies with a domainThe domain string is not on the registered domain of any host the attachment reaches: the attach url's, a goto target's, or a declared one. Playwright would match nothing silently; FKN refuses, so a sign-out naming the wrong host fails loudly instead of leaving the session.Name a domain on one of those sites, or declare the site in domains.No
frame.clearCookies: this FKN page predates clearCookies; reload the app to load the current oneLocatorUnsupportedError (terminal)cloud frame.clearCookiesThe fkn.app page that answered, served from the browser's cache, is older than the operation. Nothing was sent.Reload the app, then call again.Yes, after a reload
frame.clearCookies: this render proxy predates clearCookies; reload the appLocatorUnsupportedError (terminal)cloud frame.clearCookiesThe render proxy behind the page is older than the operation. Nothing was removed.Reload the app, then call again.Yes, after a reload
frame.clearCookies: the cookies are gone from this attachment, but its jar did not take the removal, so other attachments may still send them; call it againErrorcloud frame.clearCookiesThe cookies left this attachment, and the write that commits the removal to the jar failed, so another attachment on the same jar, and one created later, may still carry them.Call it again: a repeat is harmless.Yes
frame.clearCookies: the render proxy did not answer within 30000ms; the cookies may or may not be gone, and calling it again is safeTimeoutErrorcloud frame.clearCookiesNo answer within 30000 ms. Whether the cookies were removed is unknown.Call it again.Yes
frame.clearCookies: an extension frame runs on the browser's own cookies (cookies: 'native'), which FKN does not clear; attach with cookies: 'persistent' to clear the app's jarExtensionOperationUnsupportedError, operation 'clearCookies'extension frame.clearCookiesAn extension frame runs on the person's own browser session, which belongs to them and not to the app. Refused before anything is dispatched.Attach with cookies: 'persistent' for a session the app can end, or leave the browser's own cookies to the person.No
frame.on: the event type must be 'message' or 'document', not "<type>"TypeErrorframe.on, frame.offThe event type is neither of the two a Frame reports.Pass 'message' or 'document'.No
frame.on: the listener must be a functionTypeErrorframe.onThe second argument is not a function.Pass a function.No
frame.<method>: this backend cannot <what the method does>ErrorrequestPermissions, evaluate, addScriptTag, clearCookies, postMessage and on on a Frame that createFrame built from a backend lacking that methodThe backend handed to createFrame does not implement the method, so the Frame refuses it by name rather than answering as if it ran. The Frames attachFrame returns implement every one of them, refusing what a backend does not serve with that backend's own message.Implement the method on the backend, or do not call it.No

What each backend (the cloud, the extension or the desktop) refuses is on frames, and how long goto waits is on timeouts.

Every selector and action on a Locator can fail with these errors, and so can the frameLocator, owner and contentFrame calls and frame.fetch:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
No elements foundLocatorError, and at the deadline a TimeoutError carrying itclick, fill, hover, textContent, getAttribute, isVisible, videoElementThe chain resolved to nothing. Each attempt fails with a LocatorError and is retried; at the deadline the call rejects with a TimeoutError carrying this message, the last attempt's LocatorError as its cause.Widen the selector, or raise timeout.Yes, to the deadline
Strict mode violation: locator resolved to <n> elementsLocatorError, and at the deadline a TimeoutError carrying itthe same operationsThe chain matched more than one element and the operation needs exactly one.Narrow the selector. count and exists accept any number.Yes, to the deadline
Strict mode violation: expected <name>, got <tag>LocatorError, and at the deadline a TimeoutError carrying itvideoElement and other tag-pinned operationsThe single match is the wrong element type.Fix the selector.Yes
Locator timeout (<ms>ms): <operation>TimeoutErrorany operationThe deadline passed with no attempt having failed first, for example a single attempt that hung. A TimeoutError with no cause, since @fkn/lib 0.9.42.Raise timeout, or find out why the attempt hangs.No
fill: element <tag> is not fillableError, and at the deadline a TimeoutError carrying itfillThe element is not an <input>, <textarea> or contenteditable node.Target a real field.Yes, pointlessly
<operation>: timeout must be a positive number of milliseconds, not <timeout>; every call ends at its deadline, so 0 does not turn it offTypeErrorevery action and frame.goto, before anything is gated or sent, since @fkn/lib 0.9.42timeout was 0, below 0, or not finite. Playwright reads 0 as no deadline; here every call ends by its deadline, so 0 is refused rather than failing at once or waiting forever.Pass a positive number of milliseconds, or leave timeout out for 30000.No
<operation>: position is Playwright's offset in pixels, which no backend serves yet; pass relativePosition, a 0..1 fraction of the boxTypeErrorclick and hover with a position option, before anything is sent, since @fkn/lib 0.9.42position was the name of the 0..1 fraction of the box before 0.9.42. Playwright's position is in pixels, which no backend serves yet, so the key is refused rather than read as a fraction.Rename it to relativePosition, with the same values.No
frameLocator.owner: this frame was not entered through frameLocator(selector)TypeErrorowner() on a FrameLocatorowner() is the Locator on the iframe a frameLocator(selector) entered, and this chain did not end in one.Call owner() straight after frameLocator(selector), or locate the iframe with locator(selector).No
locator.contentFrame: only a locator that ends in locator(selector) can enter its frame; use frameLocator(selector)TypeErrorcontentFrame() on a LocatorcontentFrame() enters the iframe a trailing locator(selector) matches, and this chain ends in another selector.Call frameLocator(selector) on the chain instead.No
frameLocator: no <iframe> matchedLocatorErrorany call whose chain crosses a frameLocatorThe barrier selector matched nothing.Wait for the iframe, or fix the selector.Yes, to the deadline
frameLocator: expected <iframe>, got <<tag>>LocatorErrorany call whose chain crosses a frameLocatorThe barrier's first match is not an iframe.Fix the selector.Yes
frameLocator: this realm has no bridge into child framesLocatorUnsupportedError, operation 'frameLocator'any call whose chain crosses a frameLocatorThe realm cannot descend.Do not descend from this realm; drive the child from a realm that has a bridge.No
frameLocator: <iframe> has no contentWindowErrorany call whose chain crosses a frameLocatorThe iframe holds no window yet.Wait for it to load.Yes
the frame this call was routed to went away mid-call, retryingLocatorErrorany call across a frameLocator hopThe child frame navigated mid-call.Let the dispatch loop retry; no handling is needed.Yes, to the deadline
the attached frame became stale during the call, retryingLocatorErrorany call on an extension FrameThe attached frame navigated mid-call.Let the dispatch loop retry; no handling is needed.Yes
frame-locator: the target frame has no window yet, frame-locator: registration <id> went away before it answered, retryingLocatorErrorany call across a window bridgeBridge registration raced the call.Let the dispatch loop retry; no handling is needed.Yes
Unknown locator kind: "<kind>", Unknown selector: "<kind>.<name>"LocatorUnsupportedErrora chain built with a selector this stack does not registerThe selector module is not in the registry. @fkn/lib's Locator is pre-bound to the extension stack's registry, which adds videoElement and the reason option.Use the registered selectors. See Locators and actions.No
Locator operation not supported on the <backend> backend: <operation>LocatorUnsupportedError, with operation and backendany operation a backend refusesThe backend does not serve that operation.Use the other backend.No
requestPictureInPicture is not supported in this environment, exitPictureInPicture is not supported in this environmentErrorvideoElement().requestPictureInPicture(), .exitPictureInPicture()The framed document's browser lacks the method.Feature-detect first.No
videoElement: unknown method "<method>"Errora videoElement handle method that does not existOnly play, pause, load, requestPictureInPicture and exitPictureInPicture are callable.Call one of the five methods that exist.No
Index or size is negative or greater than the allowed amountDOMException, name IndexSizeErrorvideoElement state's buffered.start(i), .end(i), seekable.*The index is out of range on the rebuilt TimeRanges.Check length first.No
fetch: url must be a stringLocatorInvalidError, operation 'fetch'frame.fetch on either backendThe url argument is not a string.Pass a string.No
fetch: not a valid url: <url>LocatorInvalidError, operation 'fetch'frame.fetchThe url does not resolve against the landing document's baseURI.Pass a valid url.No
frame.fetch on the cloud backend needs its own session: attach with cookies: 'ephemeral'LocatorDeniedError (terminal)cloud frame.fetch, and cloud ensure('fetch'), on cookies: 'persistent', the defaultThe attachment uses the shared render proxy jar, one record shared by every app of the top-level site. No grant can lift this.Re-attach with cookies: 'ephemeral'.No
frame.fetch: url must be a stringLocatorDeniedError (terminal)cloud frame.fetch, and cloud ensure('fetch')The page-side gate sees a non-string where the url should be. ensure('fetch') hits this because the gate reads the options object as the url.Pass a url, or skip ensure('fetch') on the cloud backend.No
frame.fetch on the cloud backend needs an absolute urlLocatorDeniedError (terminal)cloud frame.fetchA relative url resolves in the landing realm, which the app page cannot know.Pass an absolute url.No
frame.fetch: only http(s) urls are supportedLocatorDeniedError (terminal)cloud frame.fetchThe scheme is neither http nor https.Use http or https.No
fetch: only http(s) urls are supportedLocatorDeniedError (terminal)extension frame.fetchThe same rule as the row above, decided in the content script rather than on the page, so the text carries no frame. prefix.Use http or https.No
fetch: the url resolves outside the site this call namesLocatorDeniedError (terminal)extension frame.fetchThe url read on its own and the url resolved against the landing document's base name different hosts, so the host the consent row would name is not the host the request would reach. //other.example/x and a page-set <base href> are the usual forms.Pass an absolute url whose host is the one you mean.No
frame.fetch: the target is outside the origins this attachment declaredLocatorDeniedError (terminal)cloud frame.fetchThe hostname is not in domains and the origin is neither the attach target nor a goto target. The message deliberately does not echo where the call tried to go.Declare the host in domains at attach, or goto it first.No
frame.<operation>: the user did not grant <category> on <host>, ending in `here` when the frame names no hostLocatorDeniedError (terminal)cloud frame.fetch, and every gated cloud locator callThe consent card was refused or dismissed. A dismissal starts a 10000 ms cooldown during which the card is not shown again and the call fails closed. A document-bound operation ends <category> here instead, naming no host, since the frame may have moved somewhere your app never named.The message opens with the operation, so test it with message.includes(...) rather than startsWith. Ask again after the cooldown, or explain why the app needs it.Yes, after the cooldown and a grant
frame.<operation>: the user has not granted <grant> here, and frame.fetch: the user has not granted network on <host>LocatorDeniedError (terminal)every gated operation on the cloud backendThe render proxy page re-ran the same policy and found no stored grant, without prompting. It checks every gated operation, not just frame.fetch. A document-bound operation ends in here and names the grant, which is the category id for an ordinary key and the key itself for a critical one; a fetch names network and the target host instead.The message opens with the operation, so test it with message.includes(...) rather than startsWith. Ask again later, or explain why the app needs it.Yes
frame.fetch: the call completed but its audit receipt could not be recorded; result withheldLocatorDeniedError (terminal)extension frame.fetchThe fetch ran but its activity-log receipt could not be written. The result is withheld rather than returned unrecorded, and the name is terminal so the loop does not re-issue a state-changing request.Treat the request as having happened with an unknown result.No
frame.fetch: <error>; not retried, since the request may already have been sentLocatorDeniedError (terminal)extension frame.fetch, from the first extension store release after 0.1.54The request failed in the landing document after it may have left: a network error, a cross-origin read the browser refused, or a redirect hop onto an FKN platform host that the extension blocked. <error> is the browser's name: message, such as TypeError: Failed to fetch. The call ends there rather than being sent again every 50 ms to the deadline, so a POST goes out at most once. Through 0.1.54 the loop sent it again until the deadline and ended with a TimeoutError.Treat the request as possibly sent. A cross-origin url has to answer the landing document with CORS, and its redirects have to stay off FKN hosts.Sometimes, after a network error; never for a refused read or a blocked hop
fetch on the shared render proxy session, fetch on a frame holding no proxied document; navigate first, fetch is not available while the proxied document is on its own originLocatorUnsupportedError, backend 'render proxy'cloud frame.fetchThe render proxy's own refusals. The third fires whenever the proxied document sits on its own frame-host origin, which is the deployed default, so cloud frame.fetch normally ends here.Use the extension backend for frame fetches.No

A timeout rarely says so in its message. The retry loop runs until its 30,000 ms deadline and then rejects with a TimeoutError carrying the last attempt’s message and that attempt as cause, so test the class, see what a timeout says. isTerminalError matches the three error names that stop the loop early, listed on locators and actions.

Every gated extension call can fail with the consent refusal. It is the error your call gets when a user denies a row on the consent sheet, the prompt the extension shows before an action above severity 0. A row is one category of access on one website, so a refusal covers every capability in that category there:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
Permission denied: <category> on <site> (<key> <scope>), Permission denied: <category> (<key>)PermissionDeniedError, with grantKey, category, site, hosts, permissionKey and scopeevery gated extension call: fetch with credentials: 'include', cookies.get, setRequestHeaderRule, every locator operation, frame.goto, frame.fetch, extension.attachFrameThe user refused, dismissed the consent sheet, or a stored deny covers that category on that site. The message names the row first and the concrete call in brackets, and a call that resolved no site drops the on <site> half. A dismissal records deny with remember: 'once', so a retry asks again. The gate raises it before the retry loop, so it is not retried.Match error.name === 'PermissionDeniedError', or use the exported isPermissionDenied. isLocatorDenied and isTerminalError do not match this name, which is the single most common wrong assumption in this area. See Permissions and consent.Yes if the user changes their mind; no while a session or always deny stands
permissions.request: pass hosts, the sites the grant applies toTypeErrorthe category form of permissions.requestA category ask carried no hosts, or none of them survived normalisation. A grant row is keyed by hostname, so there is no row such an ask could be drawn on.Pass hosts, the sites the grant applies to. The pre-category { key, scope } form needs none and is unaffected.No
frame.requestPermissions: navigate the frame or declare domains firstTypeErrorcloud frame.requestPermissions, and attachFrame({ permissions }) through itThe ask has no host to name: the frame holds no document and the attach declared no domains. A grant row is keyed by host, so there is no row such an ask could be drawn on.Declare domains at attach, or goto a target before asking.No
frame.request: navigate the frame or declare domains firstTypeErrorextension frame.requestPermissions, and attachFrame({ permissions, cookies: 'native' }) through itThe row above, as the extension's content script words it: it names the method frame.request, its name before @fkn/lib 0.9.42, until an extension release renames it. A blank frame has no content script to answer a site lookup, so the declared set is the whole row there.Declare domains at attach, or goto a target before asking.No
permission rpc: the background answered with an unknown shapeErrorpermissions.request and anything that consults the storeThe background's reply did not match the expected envelope.Retry; reload the extension if it persists.Yes
Whatever response.error saysErrorpermissions.request and anything that consults the storeThe background answered a structured failure. Its store mints no sentence of its own, so the text is the message of whatever rejected under it, which is the extension's database layer in the browser; nothing in the library fixes it.Read the text.Depends

Neither locator guard matches Permission denied: <category> on <site> (<key> <scope>). Compare error.name instead, or use the exported isPermissionDenied, as the handler above does. The full flow is on permissions and consent.

Every packages.* member, in the host app and in the package it loads, fails with a plain Error that carries one of six codes. @fkn/lib/packages types that shape as PackagesError:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
packages: no FKN transport in this realm - use relayWorker to bridge workersError, code unavailableevery packages.* member except attach, onConnect, isVisible, onVisibilityChangeNeither window nor self is available.Call relayWorker(worker) from the page. See Workers.Yes, after bridging
packages.<call>: caller identity is not establishedError, code unavailablepick, install, uninstall, connect, mount, show, hideThe broker could not attribute the call to an app identity.Retry after the broker connection settles.Yes
packages.<call>: <uri parse failure>Error, code invalidinstall, uninstall, connect, mount, show, hideThe uri failed to parse. Underlying sentences: A source uri must be a string of 1 to 512 characters, '<input>' has no '<handler>:' prefix, No handler for '<handler>:', and packages: '<handler>:' is not served by the npm registry.Pass npm:<name>.No
packages.install: version must be a valid npm version stringError, code invalidinstallThe pinned version fails the version pattern.Fix the version.No
packages.install: could not persist the install record (storage may be full or blocked)Error, code unavailableinstallThe broker could not write the install record.Free space, or check that storage is not blocked for the origin.Yes
packages.search: query must be an object, packages.search: type must be a short lowercase token, packages.search: id must be a short lowercase token, packages.search: unknown origin '<origin>'Error, code invalidsearch, pickThe query is malformed.Fix the query.No
packages.search: the npm registry did not answerError, code unavailablesearch, pickThe registry request failed.Retry.Yes
packages: could not resolve '<name>' from the npm registryError, code unavailableinstall, pickThe packument fetch failed or answered a non-object.Retry.Yes
packages: '<name>' has no latest version, packages: '<name>' has no version '<version>'Error, code invalidinstall, pickThe registry knows the package but not that version.Pick a published version.No
packages: another package prompt is already openError, code unavailablepick, installThe broker's exclusive UI slot is taken.Wait for the open prompt to close, then retry.Yes
packages.<call>: '<uri>' is not installed by this appError, code not-installedconnect, mountThis app holds no install record for the package.Call packages.install(uri) first.Yes, after install
packages.show: '<id>' is not connected by this app - connect() before showing itError, code not-installedshowThe package is installed but not connected.Call connect(uri) first.Yes
packages.<call>: '<uri>' has been disabled by the platformError, code deniedconnect, mountThe platform has switched the package off.Remove the package from the app; nothing app-side can lift this.No
packages.show: a package cannot place its own frameError, code deniedshow called from inside a package tenantOnly a host app may place a frame.Place the frame from the host app, never from inside the package.No
packages.connect: '<uri>' was released while connectingError, code not-installedconnectThe package was uninstalled mid-connect.Reinstall and retry.Yes
packages.connect: '<uri>' failed to boot: <failure>Error, code unavailableconnectThe tenant frame reported a boot failure.Read <failure>; it is the package's own boot error.Sometimes
packages.connect: '<uri>' did not register a connection handlerError, code timeoutconnectThe tenant never called onConnect.Add a packages.onConnect handler in the package. See Packages.Sometimes
packages.connect: the package did not complete the connectionError, code timeoutconnect, mount, attachThe connection handshake on the port did not settle within 30000 ms.Retry.Yes
packages.connect: the package refused the connection, or the package's own nack textError, code unavailableconnect, mount, attachThe package's createPayload threw, or it sent a nack. The package's Error.message is used when it supplied one.Read the text; it is the package's.Depends
packages.connect: aborted before connectingError, code unavailableconnect, mount, attach with a signalThe signal aborted before or during the handshake.Treat it as the normal result of aborting.Yes
packages.connect: the package closed before connectingError, code unavailableconnect, mountThe broker's closed promise settled during the handshake.Retry.Yes
packages.show: pass an element or a rectError, code invalidshowNeither placement option was given.Pass one.No
packages.show: a rect with finite x, y, width and height is requiredError, code invalidshowThe rect has a non-finite member.Fix the rect.No
packages.mount: pass the iframe to load the package intoError, code invalidmountoptions.iframe is not an HTMLIFrameElement.Pass one.No
packages.mount: the iframe must be in the document before mounting into itError, code invalidmountA detached frame never navigates, so src would resolve into a 30 s wait for a tenant that is not booting.Append the iframe first.Yes, after appending
packages.mount: the iframe's sandbox attribute must include <tokens>, or the package cannot startError, code invalidmountA sandbox attribute without allow-scripts and allow-same-origin. The tenant boots a service worker on its own origin and needs both.Add the tokens or remove the attribute.No
packages.mount: this page is cross-origin isolated, so the iframe's allow attribute must include 'cross-origin-isolated' to hand that down to the packageError, code invalidmount from a cross-origin isolated pageIsolation defaults to self, so a page that does not hand it down silently drops the package to no SharedArrayBuffer.Add cross-origin-isolated to allow before mounting.No
packages.mount: the package did not register a connection handlerError, code timeoutmountThe tenant did not report ready within 30000 ms. The frame is unmounted before this throws.Add a packages.onConnect handler in the package. See Packages.Sometimes
packages.mount: the package failed to boot, or the tenant's own failure textError, code unavailablemountThe tenant reported a boot failure.Read the text; it is the package's.Depends
packages.mount: the frame was detached before it could connectError, code unavailablemountcontentWindow went away between the ready message and the port handoff.Retry with a stable iframe.Yes

Branch on code. A worker that was never relayed has to be relayed from the page first. The codes are on packages and the worker case is on workers.

Every member of @fkn/lib/rooms, and every method on a Room, fails with a plain Error that carries one of twelve codes, typed as RoomsError:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
rooms: wrong room keyError, code bad-keyrooms.open, rooms.joinThe room exists, because somebody holds a seat or a claim keeps it, and the key presented is not the one it was opened with. rooms.open without key mints a fresh one, so it meets this whenever the name is in use.Pass room.invite whole to rooms.join, or the room's own key to rooms.open.No
rooms: the invite carries no keyError, code invalidrooms.joinThe invite has no dot, or nothing on one side of it, so @fkn/lib refuses it before the broker is asked. The broker answers the same for a key that is not 32 bytes of base64url or an id that is not a room id.Pass room.invite as it was, the key and the id joined by a dot.No
rooms: the name is not one a room can haveError, code invalidrooms.open, rooms.createThe name is empty, over 72 characters, or outside the grammar once capitals fold to lowercase: 1 to 64 of a to z, 0 to 9, ., _ and -, a letter or digit at each end, and never fkn or a name starting fkn-. The broker answers the same when the calling app has no scope a room can live under.Fold the name to lowercase and keep it to that alphabet, or let rooms.create pick one.No
rooms: the key is not 32 bytes base64urlError, code invalidrooms.open, rooms.createThe key option does not decode to exactly 32 bytes, or is not written as unpadded base64url.Pass 32 bytes as unpadded base64url, 43 characters, or leave key out so the broker mints one.No
rooms: no such memberError, code not-foundroom.grant, room.revoke, room.limit, room.remove, room.block, room.unblockThe id names nobody this room holds, or, for unblock, nobody it has blocked.Take ids from room.members() and from the room events, and drop one on a left event.No
rooms: no such messageError, code not-foundroom.edit, room.deleteThe mailbox holds no message under that seq, or nothing in the range, because it was deleted or never stored.Take seq from a message event or a backlog page, and drop it on a deleted event.No
rooms: the room is fullError, code fullrooms.open, rooms.joinThe room already seats its member cap, counting members inside the hold.Wait for a member to leave. The cap is set once, by the members option of the join that brought the room into being, and is at most 100.Yes
rooms: too many connectionsError, code fullrooms.open, rooms.joinThis member already holds the connection cap in this room, across its tabs and devices.Leave the room in a tab that no longer needs it, then open it again.Yes
rooms: too many blocksError, code fullroom.blockThe room's block set is full, and it refuses rather than dropping an entry.Unblock somebody first, since dropping an entry would let them back in.Yes, after an unblock
rooms: too many claimsError, code fullroom.claimThe account already holds its claim cap: 10 claims, or 2 under global for a name there. The claim waited on the room limit card fkn.app showed over your app, and the person closed it without freeing a slot, or fkn.app could not show it.The person can unclaim a room on that card or with Unclaim in the Rooms tab of its fkn.app settings, and the app that claimed a room can release it. Claim again when they ask, not in a loop, since each claim at the limit shows the card again.Yes, after a release or an Unclaim
rooms: you are blocked from this roomError, code blockedrooms.open, rooms.joinA member holding block blocked this member, by its id and by its account, or for a guest by its network, and a block lives as long as the room.Nothing app-side lifts it. A member holding block can call room.unblock(id).No
rooms: you cannot send hereError, code deniedroom.sendThis member's send is false, from the room default or from a revoke.Read room.self.permissions.send and hide the composer while it is false.Yes, after a grant
rooms: permission deniedError, code deniedroom.remove, room.block, room.unblock, room.edit, room.delete, room.backlogThe caller lacks what the call needs: remove or block for those calls, receive for backlog, and for edit and delete a message of its own, one at a time for delete, a rule only the owner is exempt from.Offer each control only while room.self.permissions carries what it uses, and edit or delete only your own messages unless you own the room.Yes, after a grant
rooms: not the room ownerError, code deniedroom.setDefault, room.limit, room.grant, room.revoke, room.claim, room.setMailbox, room.releaseOnly the owner sets defaults and limits, moves permissions, claims or releases the room, and turns a claimed room's mailbox off or on. In a claimed room the owner is the claiming account, and in a room under an app's scope it acts as the owner only through the app that claimed it (`rooms: only the app that claimed the room can change it`, and for release `rooms: only the app that claimed the room can release it`).Compare room.self.id with room.owner before offering these calls.No
rooms: the owner cannot be removedError, code deniedroom.remove, room.blockThe target is room.owner, and nobody removes or blocks the owner.Render no remove or block control against the owner.No
rooms: the owner keeps every permissionError, code deniedroom.grant, room.revoke, room.limitThe target is room.owner, whose four permissions and message cap are constant.Skip the owner when you render per-member permission controls.No
rooms: you cannot remove yourselfError, code deniedroom.remove, room.blockThe target id is room.self.id.Call room.leave(), which is the call that means leaving.No
rooms: claiming needs an accountError, code deniedroom.claimThis connection was opened with no FKN account connected, or the account no longer exists.Connect the account before opening a room you mean to claim. A connection keeps the sign-in it opened with, and connecting can give the person a new member id, which is then not the owner.Yes, in a room opened after connecting
rooms: claiming needs premiumError, code deniedroom.claim, room.setMailbox(true)The connected account is not premium, as the room read it when this connection opened or as the service answers now. Turning a claimed room's mailbox on needs premium as a claim does, even when it is on already, and turning it off never does.Offer the claim only to a premium account, and after an upgrade open the room again so the connection carries it.Yes, after an upgrade and a reopen
rooms: the name is claimedError, code deniedroom.claimAnother account holds the claim on this name, or claimed it while this request was on its way.Pick another name. A claim stays with its account until it is released or lapses, or the account unclaims it in fkn.app.No
rooms: the description is not one a claim can carryError, code invalidroom.claimThe description is not a string, or once trimmed it is empty, over 200 characters as String.length counts them, or holds a control character, a line or paragraph separator, a bidi control or a lone surrogate. @fkn/lib refuses a value that is not a string before the broker is asked, and the rooms service refuses the rest, so nothing about the claim changes.Pass one line of plain text of 1 to 200 characters, shortening it yourself, or claim without a description, which keeps the stored one. An empty one is refused too, so a description cannot be removed: it goes only with the claim.No
rooms: only the app that claimed the room can describe itError, code deniedroom.claimThe account already holds the claim of a room under an app's scope, any scope but global, made from another of its apps, as when this app reached the room by invite. There a claim with a description replaces the stored one only from the app that claimed the room, the same app once it is verified, so the account never reads one app's words under another app's name. In a global/ room any app of the account describes it.Claim without a description, which still resolves for the account, or describe the room from the app that claimed it.No
rooms: the temporary duration is not one a claim can carryError, code invalidroom.claimThe temporary option is neither a boolean nor a whole number of milliseconds from 60,000 to 31,536,000,000, 1 minute to 365 days. @fkn/lib refuses it before the broker is asked, and the rooms service checks the range again, so nothing about the claim changes.Pass true for 7 days, or a whole number of milliseconds inside the range. ROOM_TEMPORARY_MS from @fkn/lib/contract carries the default and both bounds.No
rooms: only the app that claimed the room can set when it expiresError, code deniedroom.claimThe account already holds the claim of a room under an app's scope, any scope but global, made from another of its apps, and this claim passed temporary. There only the app that claimed the room, the same app once it is verified, sets or changes when its claim ends. In a global/ room any app of the account does.Claim without temporary, which still resolves for the account and leaves the room as it was, or set the duration from the app that claimed it.No
rooms: temporary claims are not availableError, code unavailableroom.claimThe claim passed temporary, and the broker holding the room is older than that option, so it would drop it and make the claim permanent. @fkn/lib refuses rather than keep a room longer than the app asked for.Nothing app-side makes an old broker carry it: a claim without temporary still works there, and the person gets a current broker the next time fkn.app loads.Yes, once fkn.app has updated
rooms: the mailbox switch is not availableError, code unavailableroom.claim with mailbox: false, room.setMailboxThe broker holding the room is older than the mailbox switch, so it would drop mailbox: false and make a claim that keeps messages, and it has no setMailbox. @fkn/lib refuses rather than store what the app asked it not to, with nothing sent.Nothing app-side makes an old broker carry it: a claim that keeps a mailbox still works there, and the person gets a current broker the next time fkn.app loads.Yes, once fkn.app has updated
rooms: only the app that claimed the room can release itError, code deniedroom.releaseThe account holds the claim of a room under an app's scope, any scope but global, made from another of its apps. Only the app that claimed such a room releases it, the same app once it is verified, so the claim and its mailbox stand. Any app of the account releases a global/ room.Release it from the app that claimed it, or unclaim it from the Rooms tab in fkn.app.No
rooms: only the app that claimed the room can change itError, code deniedroom.setDefault, room.limit, room.grant, room.revoke, room.remove, room.block, room.unblock, room.setMailbox, room.edit, room.deleteThis room is under an app's scope, any scope but global, and the account claimed it from another of its apps: only the app that claimed it, the same app once it is verified, changes it. It holds even where room.self.id equals room.owner, as it does for an account whose encryption key the browser holds, and for the account's own messages, since a member id belongs to the account and not to the app. Nothing in the room changes.Make the change from the app that claimed the room, or from the Rooms tab in fkn.app for what it offers. This app still joins, reads and sends there, and a global/ room takes these changes from any app of the account.No
rooms: the room is not claimedError, code invalidroom.release, room.setMailboxThe room holds no claim to release, or whose mailbox to turn off or on.Read room.claimed, or wait for the claim event, before offering a release or the mailbox switch.No
rooms: the room keeps no messagesError, code invalidroom.edit, room.deleteThe room has no mailbox, so it stored nothing it relayed: nobody has claimed it, or its claim keeps no messages, made with mailbox: false or turned off since with room.setMailbox(false) or the Messages kept switch in the owner's fkn.app settings. While it keeps none the room refuses a size limit from those settings the same way, since a mailbox turned on later starts with no limit, and fkn.app says so on the room's row: no app call receives that one.Offer edit and delete only while room.mailbox is not null, and drop them on a claim event whose mailbox is false.No
rooms: the limit is out of rangeError, code invalidroom.limitmaxMessageBytes is not a whole number from 1 to 33,554,432.Clamp the value to that range, or pass null with a member id to clear that member's override.No
rooms: sending too fastError, code rate-limitedroom.sendThe per-member or per-room rate is spent. Ten such refusals inside ten seconds close the connection, and the broker dials it again.Queue the text and send it again a moment later, and never in a tight loop.Yes
rooms: the message is too largeError, code too-largeroom.send, room.editThe sealed message, nonce and ciphertext in base64url, is over this member's maxMessageBytes: 262,144 by default, and never over the platform ceiling. Nothing is trimmed.Read room.self.maxMessageBytes and split the text to fit. Four thirds of its UTF-8 byte length, plus 39, never undercounts the sealed size.Yes, with shorter text
rooms: the mailbox is fullError, code storageroom.send, and room.edit when the text growsThe claimed room's mailbox would pass 500,000,000 bytes, or the lower size limit the account set for the room in its fkn.app settings, and it refuses rather than dropping old messages.Delete old messages with room.delete as the owner, then send again, and read cap from room.usage() for the limit that applies. A room that needs no history can stop keeping messages instead, with room.setMailbox(false), which deletes what it kept.Yes, after a delete or a higher limit
rooms: the owner is out of storageError, code storageroom.send, and room.edit when the text growsThe claiming account is over its storage quota, files, mailboxes and stored objects together, as the service last answered.Free storage in the owner's account, by deleting files or by clearing, unclaiming or turning Messages kept off for a room in the Rooms tab of its fkn.app settings, then send again a minute later: a refused write makes the room ask the service again within a minute.Yes, once the owner has room
rooms: out of daily bandwidthError, code quotaroom.send, room.edit, room.backlogThis free account, or this guest's network, has spent the day's free volume, which room traffic in both directions shares with cloud egress. A premium account is not metered.Wait for the next UTC day, or open the room again signed in on a premium account. Messages keep arriving meanwhile.Yes, the next UTC day
rooms: rooms are unavailableError, code unavailablerooms.open, rooms.create, rooms.join, and every member of a joined RoomThere is no broker to ask, the broker was replaced while the call was pending, the connection could not be opened or was lost, the platform refused this connection's sign-in, the service behind a claim did not answer, or a claimed room under an app's scope could not find out whether this app is the one that claimed it.Check rooms.available() first, and open the room again from the invite.Yes
rooms: malformed frameError, code invalidrooms.open, rooms.create, rooms.join, and every member of a joined RoomThe platform could not read a frame, or a value in it is outside what the frame allows: limit(null) with no member id, a delete range that runs backwards, a backlog limit under 1, a defaults.maxMessageBytes over the platform ceiling, or a mailbox option on claim or an argument to setMailbox that is not a boolean, which @fkn/lib refuses before the broker is asked. The broker answers the same to a claim or release from a caller it cannot name as an app, and the rooms service to a claim or release naming another app than the one the connection joined as, which @fkn/lib and the broker never send.Pass null to limit only with a member id, and keep ranges, counts and sizes positive and in order.No
rooms: the room has endedError, code closedevery member of a joined Room, once it has ended for this appThe room is over here: you left, in this tab or another, you were removed or blocked, or the rejoin window passed.Read await room.closed for the reason, and call rooms.join(invite) for a fresh Room.No, this Room is finished

Branch on code, never on the text: the wordings are the library’s own literals, kept here so the catalogue can find them, and a code survives a rewording. Which code each method raises is on rooms.

Every member of @fkn/lib/storage, and every read of the blob storage.get resolves, fails with a plain Error that carries one of eight codes, typed as StorageError. An aborted signal rejects with the signal’s own reason instead:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
storage: the data is not a Blob or a ReadableStreamError, code invalidstorage.putThe data is neither a Blob, a File included, nor a ReadableStream, or a stream delivered a chunk that is not a Uint8Array. A wrong type is refused before the broker is asked.Pass the File or Blob itself, or a ReadableStream<Uint8Array> with its size.No
storage: a stream needs the size it will deliver, a whole number of bytesError, code invalidstorage.putA ReadableStream came without size, size is not a whole number of bytes from 0 up, or a Blob came with a size other than its own.Pass size as exactly the bytes the stream delivers, and leave it out for a Blob.No
storage: the stream did not deliver the size it declaredError, code invalidstorage.putThe stream ended before size bytes, or carried on past them. The broker aborts the upload, and the storage it reserved is released.Declare the exact length of what the stream delivers, or pass a Blob.No
storage: the file is too large to storeError, code invalidstorage.putThe file is larger than 167,772,160,000 bytes, 10,000 parts of 16 MiB, the largest object the service lays out. Nothing was uploaded.Split the file across several objects and keep their urls together in your app.No
storage: the key is not 32 bytes base64urlError, code invalidstorage.put, storage.getThe key is not a string, or it does not decode to exactly 32 bytes written as canonical unpadded base64url. Nothing was sent.Pass the key that put answered, unchanged, or 32 random bytes as unpadded base64url, 43 characters. Leave it out of put to have one minted.No
storage: the url does not name a stored objectError, code invalidstorage.get, storage.deleteThe url is not a string, or it is not exactly the service's address followed by a lowercase UUID: another host, an uppercase id, a trailing slash, a query or a path below the id are refused before any request.Pass the url that put or list answered, unchanged.No
storage: the limit is out of rangeError, code invalidstorage.listThe limit is not a whole number from 1 to 1,000.Pass 1 to 1,000, or leave limit out for pages of 1,000.No
storage: the cursor is not one list answeredError, code invalidstorage.listThe cursor is not in the shape a page of list answers.Pass the previous page's cursor unchanged, and none for the first page.No
storage: no object at this urlError, code not-foundstorage.get, and every read of the blob it resolves: stream, arrayBuffer and a slice's; storage.put when the upload is deleted while it runsNo object is ready at the url: it was never made, it was deleted, or it is still uploading, one answer for all three. A read that starts after a delete meets it, while a read already streaming when the delete landed finishes.Treat the file as gone, and ask whoever shared the url for a new one.Yes for an object still uploading, once it is ready, and no otherwise
storage: the object does not match its keyError, code integritythe first read of the blob storage.get resolves: stream, arrayBuffer and a slice's; storage.get itself when the stored size is not one the format producesA 1 MiB record did not open under the key: the key is not the one the object was sealed with, or the bytes are not this object's. get resolves before any record is read, so a wrong key shows on the first read.Read with the key that put answered for this url. Asking again with the same key gives the same answer.No
storage: storing needs an accountError, code deniedstorage.put, storage.list, storage.deleteNo FKN account is connected to this app. Only get works without one.Call connect() first, and check storage.available() before offering an upload.Yes, once an account is connected
storage: only the app that stored the object can delete itError, code deniedstorage.deleteThe object belongs to this account, and another app stored it. The app that stored it is the only one that deletes it, the same app once it is verified.Delete it from the app that stored it. To share deletion, have that app do it on request.No
storage: storage quota exceededError, code quotastorage.putThe account's storage quota, its files, claimed room mailboxes and stored objects together, has no room for the sealed size, either when the upload starts or when it completes.Delete objects or files the app no longer needs, or tell the person their storage is full.Yes, once space is freed
storage: too many stored objectsError, code too-manystorage.putThe account already holds 10,000 objects, uploads in progress included, counted apart from its cloud.fs files.Delete objects the app no longer needs before storing more.Yes, after a delete
storage: too many objects stored this hour, try again laterError, code too-manystorage.putThe account has started 1,000 uploads this hour on a free plan, or 5,000 on premium. The hour opens with the first upload it counts.Queue the upload and send it once the hour is over, rather than retrying in a loop.Yes, once the hour is over
storage: the signed-in account changedError, code account-changedstorage.put, storage.list, storage.deleteThe account this page was on when the call was made is no longer the one signed in, so nothing was stored, listed or deleted for the new one.Read the account again, and repeat the call for the new account only if the person asks for it.No, a new call acts for the new account
storage: storage is unavailableError, code unavailableevery member of @fkn/lib/storage, and every read of a blob storage.get resolvedNo broker serves storage here, as in Node or on a broker older than storage, or the broker, the service or the store did not answer after its retries, or a refusal arrived whose code this library does not know.Check storage.available() before an upload, and try the call again later.Yes

Branch on code, never on the text. These messages share the storage: prefix with the file system rows above, so a startsWith('storage: no object at') matches a missing file and a missing object alike, and storage: the signed-in account changed is also how the cloud.fs account change error, storage: the signed-in account changed, so this call was not made, begins. Which call raises which code is on object storage.

The bounded wait for a broker, a broker replaced while a call was pending, the two rejections of an awaited relayWorker call, and a call into the extension that its stopped worker cut off all land here. The last two rows come with the next FKN extension store release, the first after 0.1.54:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedBrokerUnreachableErrornet.Socket#connect, net.Server#listen, dgram.Socket#bind, and anything else built on apiWithin from @fkn/lib/apiNo broker connection settled within the deadline: 8000 ms, and once any call has missed it, every later call waits only 1000 ms.Use apiWithin(what) wherever the caller owns a socket, a timer or a UI: it is the bounded alternative to apiPromise, which never rejects and parks forever in a realm with no broker. See Connection and lifecycle.Yes
FKN: the broker was replaced while this call was pending; retry itErrorany call in flight when the broker document is replaced (its update flow reloads the broker frame)A call is bound to the broker connection it started on. When that document is replaced, pending calls are rejected rather than left to hang silently; the new connection is already routed.Match message.startsWith('FKN: the broker was replaced') and retry once.Yes, and the message says so
FKN @fkn/lib: relayWorker must be called from the main threadErrorrelayWorkerNo window.Call it from the page.No
FKN @fkn/lib: relayWorker found no FKN transport in this realmErrorrelayWorkerThis realm has neither a mounted broker frame nor a MessagePort granted by a parent FKN realm.Import the library on a page that mounts the broker frame before calling relayWorker.Yes, once the transport is up
the FKN extension's background stopped before it answeredBackgroundStoppedErrora call into the extension while Chrome stops its worker: extension.fetch and the root fetch on the extension path, cookies.get, the two header rule calls, and attachFrame and goto on cookies: 'native', from the next FKN extension store release onChrome stops an extension's service worker once it has been idle for a while, and this one stopped before the call resolved. The call may or may not have run, so the extension never sends it again, and the next call is answered by the restarted worker. Raised from the first store release after 0.1.54; through 0.1.54 a call made after such a stop stays pending until the page reloads.Match it with isBackgroundStopped(), then repeat the call yourself only when running it twice is harmless, such as a GET. See a call the extension never finished.Yes, as a new call of yours, never resent by the extension
the FKN extension's background stopped while the response body was still arrivingBackgroundStoppedErrorreading the body of an extension.fetch response, or of the root fetch on the extension path, from the next FKN extension store release onThe response had resolved and its body was still streaming from the extension when Chrome stopped its worker, so the read rejects rather than ending early. The request is never sent again. Raised from the first store release after 0.1.54, as the row above.Discard what was read, and repeat the fetch only when running it twice is harmless.Yes, as a new call of yours, never resent by the extension

connection and lifecycle lists the calls that wait with no deadline of their own.

@fkn/lib and the extension declare nine errors that no routed call reaches. They are listed here so that you do not write a handler for them:

MessageName or codeWhy it cannot fire
owner: this realm has no bridge to its parent frameLocatorUnsupportedError, operation 'owner'Since @fkn/lib 0.9.42 owner() is Playwright's: on a FrameLocator it is the Locator on the iframe it entered, built in the page from parts every backend resolves, and the Frame has no owner(). Nothing in the library sends the upward hop that raises this.
owner: already at the top frameLocatorUnsupportedError, operation 'owner'Since @fkn/lib 0.9.42 owner() is Playwright's: on a FrameLocator it is the Locator on the iframe it entered, built in the page from parts every backend resolves, and the Frame has no owner(). Nothing in the library sends the upward hop that raises this.
owner: the parent frame is outside this bridge boundaryLocatorUnsupportedError, operation 'owner'Since @fkn/lib 0.9.42 owner() is Playwright's: on a FrameLocator it is the Locator on the iframe it entered, built in the page from parts every backend resolves, and the Frame has no owner(). Nothing in the library sends the upward hop that raises this.
Locator operation not supported on the cloud backend: <operation>LocatorUnsupportedErrorThe cloud backend's set of unsupported operations is empty, so the guard before every call never matches, and the render proxy registers videoElement as well, so nothing refuses it either.
rooms: rooms are not availableError, code unavailableThe library carries the wording and never throws it, so a broker with no rooms service to dial answers rooms: rooms are unavailable instead.
rooms: already joinedError, code invalidThe broker sends exactly one join per connection, the first frame after it opens, and every open, create and join opens a connection of its own, so no call reaches the second join this refuses.
rooms: not joinedError, code invalidThe broker sends a join first on every connection, and every path that takes a member's seat away closes that member's connections in the same step. A closed connection carries no reply, so a request caught there settles rooms: rooms are unavailable or rooms: the room has ended instead.
The FKN desktop app is not connected, desktop.<name> is unavailableErrorNo automatic routing reaches the desktop backend: desktop.available() is hardcoded false, so the root fetch never falls to it, and only a direct desktop.* call sees the throw.
attachFrame: this FKN WebExtension does not serve the window option "<key>", so it refuses the attach rather than ignore it. Updating the extension may add it.TypeError@fkn/lib 0.9.47 sends a window's url and domains alone, and every extension that serves a window reads both. Only a later library sending a new window option to an older extension can meet it.

The three owner: rows belong to the upward hop the raw locator wire still carries: since @fkn/lib 0.9.42 owner() is Playwright’s, a Locator on the iframe a frameLocator() entered, and nothing in the library sends that hop. Only a direct desktop.* call sees the desktop row, as backends explains. The window option row guards an extension against a later library: @fkn/lib 0.9.47 sends only the options every extension that serves a window reads.