Skip to content

fetch()

fetch() from @fkn/lib takes the same arguments as the platform’s fetch and returns the same Response. The request leaves from the FKN cloud or from the user’s own browser rather than from your page, so the page’s cross-origin rules do not apply to it. This page covers the three fetches in turn (the root fetch, cloud.fetch and extension.fetch), then the header rules and cookies.get.

A backend is where a call runs: the cloud, the extension or the desktop app. In @fkn/lib the desktop backend always reports itself unavailable, so one call to the root fetch uses the cloud or the extension, whichever the page has:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}) => Promise<Response>
fetch
} from '@fkn/lib'
const
const response: Response
response
= await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}): Promise<Response>
fetch
('https://example.org/api/catalog.json') // the extension when the page carries its marker, otherwise the cloud
const response: Response
response
.
Response.url: string

The url read-only property of the Response interface contains the URL of the response.

MDN Reference

url
// '' from the cloud, the final URL from the extension
const
const catalog: unknown
catalog
: unknown = await
const response: Response
response
.
Body.json(): Promise<any>
json
() // the app's own data

The Response has the same shape from either backend. Only response.url tells you which one answered. backends introduces the cloud and the extension.

The root fetch looks at the call and at the page, in this order. The first rule that applies wins:

  1. A target on an FKN platform host rejects with fetch refuses FKN platform domains (<hostname>) before anything else. Those hosts are listed under platform hosts.
  2. credentials: 'include' on the init pins the extension. The call never falls back to the cloud.
  3. The marker the extension’s content script puts on <html>, read at that very moment, sends the call to the extension with credentials forced to 'omit'.
  4. The desktop backend is asked and always answers no.
  5. Everything else goes to the cloud.

Step 3 reads the marker at the moment you call and waits for nothing. A call issued before the content script has marked the page goes to the cloud, even with the extension installed. When it matters where the request leaves from, await waitForExtensionExposure() first, or pin a backend with cloud.fetch or extension.fetch.

Only the init’s credentials counts. A Request built with credentials: 'include' is not read for the choice:

app.ts
// pins the extension, and rejects rather than fall back when it is missing
const
const me: Response
me
= await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}): Promise<Response>
fetch
('https://example.org/api/me', {
RequestInit.credentials?: RequestCredentials | undefined

A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.

credentials
: 'include',
reason?: string | undefined
reason
: 'Load your profile' })
const me: Response
me
.
Response.url: string

The url read-only property of the Response interface contains the URL of the response.

MDN Reference

url
// 'https://example.org/api/me', the final URL, so the extension answered
// no credentials on the init, so the marker decides, and the extension path sends no cookies
const
const anonymous: Response
anonymous
= await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}): Promise<Response>
fetch
(new
var Request: new (input: RequestInfo | URL, init?: RequestInit) => Request

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
('https://example.org/api/me', {
RequestInit.credentials?: RequestCredentials | undefined

A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.

credentials
: 'include' }))
await
const anonymous: Response
anonymous
.
Body.body: ReadableStream<Uint8Array<ArrayBuffer>> | null
body
?.
ReadableStream<Uint8Array<ArrayBuffer>>.cancel(reason?: any): Promise<void>

The cancel() method of the ReadableStream interface returns a Promise that resolves when the stream is canceled.

MDN Reference

cancel
() // cancel it either way: a cloud body holds a busy token until read or cancelled

The first call went to the extension or rejected. The second went wherever the marker pointed. Only the second was given a Request, and its credentials counted for nothing.

cloud.fetch hands the request to the proxy, the FKN service that makes requests from the cloud on your behalf. The request reaches it through the broker frame, the hidden fkn.app/api iframe the library mounts. The proxy then makes the request from its side.

The URL, the method, the headers, the body and the signal cross to the proxy. The broker, the connection your page holds into FKN, drops every other RequestInit key, including credentials, redirect, cache, mode, integrity, keepalive and referrerPolicy:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/search', {
RequestInit.method?: string | undefined

A string to set request's method.

method
: 'POST',
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: { 'content-type': 'application/json' },
RequestInit.body?: BodyInit | null | undefined

A BodyInit object or null to set request's body.

body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
query: string
query
: 'catalog' }),
})
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200 from example.org, or the proxy's own 4xx or 5xx
const response: Response
response
.
Response.url: string

The url read-only property of the Response interface contains the URL of the response.

MDN Reference

url
// '', the broker rebuilt the Response on its side
const response: Response
response
.
Response.redirected: boolean

The redirected read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected.

MDN Reference

redirected
// false, always

The status is the one the upstream returned, the upstream being the server the URL names. The Response itself is built afresh on your side, so response.url is '' and redirected is false. The cloud path never carries a cookie of the user’s and never follows a redirect: a 301 or 302 arrives as an ordinary Response with that status and a location header.

set-cookie is the one upstream header you never see, because the browser’s Response constructor drops it. statusText is '' unless the two statuses agree. It is never the upstream’s phrase, so do not parse it.

A string, an ArrayBuffer or a typed array body crosses as it is. cloud.fetch reads a Blob, FormData, URLSearchParams or ReadableStream body into an ArrayBuffer first, in your own realm (the JavaScript context your code runs in). Past 10 MiB by default the proxy answers 413 request body exceeds POST_MAX_BODY_SIZE.

Only the headers you passed travel. Nothing adds the content-type the platform would have chosen for such a body, so set it yourself.

A Request input cannot cross the broker channel, so cloud.fetch unfolds it. The URL always comes off the object, and so do the method and the headers, with the body read off a clone. An init passed beside it overrides the method, the headers and the body. init.headers replaces the request’s headers rather than merging with them.

Those headers are what the platform left after dropping the forbidden names, so a Cookie, Origin or Referer set on a Request never reaches the proxy. See headers the page cannot set. A GET or HEAD sends no body, whatever the Request held.

The Response you get back is fresh. Its body is a stream that @fkn/lib pipes from the broker. While that body is open, the library holds a busy token, one of the reasons a realm reports itself as still working. shell.busyReasons() lists it as streamed response (N).

The token is released when the body ends, errors or is cancelled, or when a Response dropped unread is garbage collected. Cancel the bodies you will not read:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
,
(alias) namespace shell
import shell
shell
} from '@fkn/lib'
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json')
(alias) namespace shell
import shell
shell
.
shell_d_exports.busyReasons(): string[]
export shell_d_exports.busyReasons

What a shell reload would sever right now, as a list of human-readable reasons, empty when nothing is bound to the broker connection.

This is the evidence for deciding whether to call applyUpdate yet: open sockets and listening servers, a streaming proxy response, a mounted package, a live frame attachment, unflushed write-behind. An empty list is not a promise that a reload is free, only that this realm holds nothing the lib knows about, so an app with state of its own should weigh that too.

It is per REALM, and that distinction has already caused a wrong reading once. A worker that imports @fkn/lib/net keeps its own tally, which this function cannot see from the window. An app whose transfers live in a worker should ask the worker, not the page.

busyReasons
() // includes 'streamed response (1)' while the body is still open
if (!
const response: Response
response
.
Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

MDN Reference

ok
) await
const response: Response
response
.
Body.body: ReadableStream<Uint8Array<ArrayBuffer>> | null
body
?.
ReadableStream<Uint8Array<ArrayBuffer>>.cancel(reason?: any): Promise<void>

The cancel() method of the ReadableStream interface returns a Promise that resolves when the stream is canceled.

MDN Reference

cancel
() // releases the token without reading
else await
const response: Response
response
.
Body.arrayBuffer(): Promise<ArrayBuffer>
arrayBuffer
() // reading to the end releases it too

A 204, 205 or 304 has no body and holds no token. connection and lifecycle explains who reads busy tokens.

Neither @fkn/lib nor the broker puts a deadline on this path. The call waits for the broker connection however long that takes. init.signal is the one way to stop a transfer from your side.

The proxy keeps two clocks of its own: 10,000 ms to reach the upstream and 30,000 ms between two reads from it. A miss answers 502 upstream fetch failed: <e> before the response head, or fails the body read after it.

cloud.fetch works from a worker the page relayed with relayWorker, because the relay gives that worker a transport into the broker. In a worker nobody relayed, the call has nowhere to go and, with no deadline, waits forever. See workers.

extension.fetch runs the request in the service worker of the extension, the FKN browser extension, in the user’s own browser. The request leaves from the user’s own connection, so no cross-origin rule applies and it can carry their sessions. It needs the extension and a window realm. It never falls back to the cloud.

What it asks the user depends on what the call spends, ranked by severity. The consent sheet is what the extension shows the user before an action above severity 0:

You passPermissionWhat happens
nothing specialnetwork.fetch, severity 0granted silently
credentials: 'include'network.fetchCredentialed, severity 3the consent sheet
a local network targetnetwork.fetchLocal, severity 3the consent sheet, asked before either of the two above

Pass a reason on every call that can prompt. permissions and consent covers what the sheet shows, how an answer is remembered, and what the activity log records.

A refusal arrives as a plain Error named PermissionDeniedError with the message Permission denied: network.fetchCredentialed (<scope>). Only name, message, stack and cause survive the hop from the content script, so there is no class to test against, only the name:

app.ts
import {
(alias) namespace extension
import extension
extension
} from '@fkn/lib'
try {
const
const response: Response
response
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch
fetch
('https://example.org/api/me', {
RequestInit.credentials?: RequestCredentials | undefined

A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.

credentials
: 'include',
reason?: string | undefined
reason
: 'Load your watch history',
})
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200, the request carried the cookies the browser holds for example.org
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
var Error: ErrorConstructor
Error
&&
var error: Error
error
.
Error.name: string
name
=== 'PermissionDeniedError') {
var error: Error
error
.
Error.message: string
message
// 'Permission denied: network.fetchCredentialed (https://example.org)'
} else {
throw
var error: unknown
error
}
}

credentials: 'include' attaches the cookies the browser holds for that URL, httpOnly ones included, which the page itself could never read. The values never pass through your JavaScript. The service worker attaches the whole cookie jar itself on the first hop.

A followed redirect carries only what the browser adds on its own. The response’s Set-Cookie is not visible to you either.

With redirect: 'follow', the default, the service worker follows the redirect and response.url is the final URL. redirect: 'manual' gives you Response.error(), with type 'error' and status 0, rather than an opaque redirect you can inspect. That is how a status 0 is revived across the hop.

The request body is read whole before it crosses, while the response body streams back chunk by chunk. A large upload therefore sits in memory three times over.

new Request(input, init) runs first, so a relative URL resolves against your page. Whatever Request refuses, such as a GET with a body, throws a TypeError before anything leaves. In a worker there is no document for the extension to mark. extension.available() answers false, and every call that reaches the extension rejects with a ReferenceError rather than a named message.

Every extension.* call that reaches the extension first waits for it to announce itself, up to 1,000 ms, or 150 ms after the page has finished loading. When it does not, the missing-extension handler runs. By default the handler opens the install card the broker draws and waits on the user with no deadline.

Install from the card and your call resumes. Dismiss it with no extension and the call rejects with The FKN WebExtension is not installed, enabled or not exposed on this page.. To decide when it appears, remove the handler before the first extension call and open the card from your own button:

app.ts
import {
const setMissingExtensionHandler: (handler: MissingExtensionHandler | null) => void
setMissingExtensionHandler
,
const promptInstall: (reason?: string) => Promise<boolean>
promptInstall
,
(alias) namespace extension
import extension
extension
} from '@fkn/lib'
function setMissingExtensionHandler(handler: MissingExtensionHandler | null): void
setMissingExtensionHandler
(null) // extension.* now rejects as soon as the marker wait is over
let
let exposed: boolean
exposed
=
(alias) namespace extension
import extension
extension
.
extension_d_exports.available(): boolean
export extension_d_exports.available
available
()
(alias) namespace extension
import extension
extension
.
const extension_d_exports.events: TypedEventTarget<ExtensionEventMap>
export extension_d_exports.events
events
.
TypedEventTarget<ExtensionEventMap>.addEventListener<"statuschange">(type: "statuschange", listener: ((event: CustomEvent<{
enabled: boolean;
}>) => void) | null, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('statuschange', (
event: CustomEvent<{
enabled: boolean;
}>
event
) => {
let exposed: boolean
exposed
=
event: CustomEvent<{
enabled: boolean;
}>
event
.
CustomEvent<{ enabled: boolean; }>.detail: {
enabled: boolean;
}

The read-only detail property of the CustomEvent interface returns any data passed when initializing the event.

MDN Reference

detail
.
enabled: boolean
enabled
// true on exposure, false when it went away
})
const
const installed: boolean
installed
= await
function promptInstall(reason?: string): Promise<boolean>
promptInstall
('Sync your library from your own account') // from your own button
const installed: boolean
installed
// true when the extension showed up while the card was open, false when it was dismissed
let exposed: boolean
exposed
// true while the extension is on the page, false once it went away

promptInstall also resolves true at once when the extension is already there, and false without asking in a worker. It draws the card through the broker frame, so on a page whose broker frame never connects it never settles. See connection and lifecycle.

Origin, Referer and Cookie are forbidden request headers, so the platform drops them from any Request you build. They are the three that @fkn/lib can put back. extension.fetch reads them off init.headers, separately from the Request it builds. The service worker then sets them with a rule that lives for the duration of the call, on that exact URL and method.

cloud.fetch forwards init.headers as given for a string or URL input. The proxy sends every name upstream except host, connection, content-length and transfer-encoding. None of this asks the user, because a cookie you supply is one your app already holds, not the user’s.

Pass a whole jar as one cookie string, because Headers folds repeated names with , while cookie pairs need ; :

app.ts
const
const jar: "session=abc123; theme=dark"
jar
= 'session=abc123; theme=dark' // one string, never a repeated cookie header
const
const mine: Response
mine
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch
fetch
('https://example.org/api/library', {
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: {
cookie: string
cookie
:
const jar: "session=abc123; theme=dark"
jar
} })
const mine: Response
mine
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200, the jar reached example.org and none of the user's cookies did
const
const proxied: Response
proxied
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/library', {
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: {
cookie: string
cookie
:
const jar: "session=abc123; theme=dark"
jar
} })
const proxied: Response
proxied
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200, the proxy sent the same jar
const
const stripped: Response
stripped
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
(new
var Request: new (input: RequestInfo | URL, init?: RequestInit) => Request

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
('https://example.org/api/library', {
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: {
cookie: string
cookie
:
const jar: "session=abc123; theme=dark"
jar
} }))
const stripped: Response
stripped
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 401, no cookie reached example.org because Request dropped it first

The shape of the input decides. A Request has already lost the three headers by the time either fetch looks at it, so a Request input supplies none of them on either path. A supplied Cookie and credentials: 'include' are two identities, so the extension refuses the call rather than letting one win silently. Every other forbidden name, such as host, stays dropped with no error.

The rule matches the first hop only, so a followed redirect carries none of these headers. It also sits on the URL and method rather than on your call, so concurrent calls to the same URL and method with different headers take turns.

If the rule for a supplied Cookie cannot be installed, such as when the URL is too long for the browser’s filter, the call rejects with fetch: could not install the header rule for a forged Cookie, so the request was not sent unauthenticated. A failed rule for Origin or Referer lets the request run without that header.

A target on the user’s own network gets its own consent, network.fetchLocal, asked before network.fetch. A LAN URL can therefore prompt where a public one is silent. isLocalNetworkUrl is the classifier the extension uses, so you can tell beforehand:

app.ts
import {
(alias) namespace extension
import extension
extension
,
const isLocalNetworkUrl: (raw: string) => boolean
isLocalNetworkUrl
} from '@fkn/lib'
const
const url: "http://192.168.1.10:8096/System/Info"
url
= 'http://192.168.1.10:8096/System/Info'
function isLocalNetworkUrl(raw: string): boolean
isLocalNetworkUrl
(
const url: "http://192.168.1.10:8096/System/Info"
url
) // true, so this call asks for network.fetchLocal first
const
const response: Response
response
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch
fetch
(
const url: "http://192.168.1.10:8096/System/Info"
url
, {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
:
var AbortSignal: {
new (): AbortSignal;
prototype: AbortSignal;
abort(reason?: any): AbortSignal;
any(signals: AbortSignal[]): AbortSignal;
timeout(milliseconds: number): AbortSignal;
}

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
.
function timeout(milliseconds: number): AbortSignal

The AbortSignal.timeout() static method returns an AbortSignal that will automatically abort after a specified time.

MDN Reference

timeout
(5_000),
reason?: string | undefined
reason
: 'Read the status of your media server',
})
const
const info: unknown
info
: unknown = await
const response: Response
response
.
Body.json(): Promise<any>
json
() // the server's answer, read from inside the user's network

It matches localhost and *.localhost, and names under .local, .internal and .home.arpa. On IPv4 it matches the loopback, private, link local and 0/8 ranges. On IPv6 it matches the loopback, the unspecified address, unique local, link local and IPv4-mapped addresses. A public hostname that resolves to a LAN address is not detected, because nothing has resolved it yet at this point.

On the cloud path the question does not arise. The proxy validates every address the target resolves to and answers 403 egress refused (non-public target) for the same URL, and for a name that does not resolve at all.

Requests the page makes on its own, such as an <img> or <video> source, never pass through fetch(). extension.setRequestHeaderRule rewrites headers on those instead. It asks for network.modifyRequestHeaders (severity 2) with the domains as scope, then installs a rule that applies to image, media, XHR and fetch, subframe, font, object and other requests from every document in this tab:

app.ts
const {
const ruleId: number
ruleId
} = await
(alias) namespace extension
import extension
extension
.
extension_d_exports.setRequestHeaderRule(rule: extension.RequestHeaderRule): Promise<{
ruleId: number;
}>
export extension_d_exports.setRequestHeaderRule
setRequestHeaderRule
({
domains: string[]
domains
: ['cdn.example.org'],
requestHeaders: extension.HeaderOperation[]
requestHeaders
: [
{
header: string
header
: 'Referer',
operation: "set" | "remove"
operation
: 'set',
value?: string | undefined
value
: 'https://example.org/' },
{
header: string
header
: 'Origin',
operation: "set" | "remove"
operation
: 'remove' },
],
reason?: string | undefined
reason
: 'Load media from the selected provider',
})
const
const video: HTMLVideoElement
video
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.createElement<"video">(tagName: "video", options?: ElementCreationOptions): HTMLVideoElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('video')
const video: HTMLVideoElement
video
.
HTMLMediaElement.src: string

The HTMLMediaElement.src property reflects the value of the HTML media element's src attribute, which indicates the URL of a media resource to use in the element.

MDN Reference

src
= 'https://cdn.example.org/clip.mp4'
const video: HTMLVideoElement
video
.
HTMLVideoElement.addEventListener<"ended">(type: "ended", listener: (this: HTMLVideoElement, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('ended', () => {
const video: HTMLVideoElement
video
.
ChildNode.remove(): void

Removes node.

MDN Reference

remove
()
void
(alias) namespace extension
import extension
extension
.
extension_d_exports.removeRequestHeaderRule(ruleId: number): Promise<void>
export extension_d_exports.removeRequestHeaderRule
removeRequestHeaderRule
(
const ruleId: number
ruleId
) // ends the rule early, no prompt
})
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.body: HTMLElement

The Document.body property represents the null if no such element exists.

MDN Reference

body
.
ParentNode.append(...nodes: (Node | string)[]): void

Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.

Throws a "HierarchyRequestError" DOMException if the constraints of the node tree are violated.

MDN Reference

append
(
const video: HTMLVideoElement
video
) // the media request goes out with the Referer above and no Origin

set needs a value and remove ignores one. The browser matches header names without regard to case. Navigations, scripts, stylesheets, WebSockets and pings are not covered.

The rule lives as long as the document that made it: the next navigation in that frame retires it. A navigation of the top frame retires every rule an iframe in the tab made. So does closing the tab. removeRequestHeaderRule ends a rule early and without a prompt, for a rule this document was issued.

When an app needs the value itself, such as a CSRF token to replay into a body, extension.cookies.get answers one named cookie for a URL. It asks for network.readCookie (severity 3) with the URL’s origin as scope. It never enumerates. It answers null both when the cookie is absent and when the browser refused:

app.ts
const
const csrf: extension.SiteCookie | null
csrf
= await
(alias) namespace extension
import extension
extension
.
const extension_d_exports.cookies: {
get: (details: extension.CookieDetails) => Promise<extension.SiteCookie | null>;
}
export extension_d_exports.cookies
cookies
.
get: (details: extension.CookieDetails) => Promise<extension.SiteCookie | null>
get
({
url: string
url
: 'https://example.org/',
name: string
name
: 'csrf_token' })
const csrf: extension.SiteCookie | null
csrf
// { name: 'csrf_token', value: '...' }, or null when absent or refused
if (
const csrf: extension.SiteCookie | null
csrf
!== null) {
const
const response: Response
response
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch
fetch
('https://example.org/api/rate', {
RequestInit.method?: string | undefined

A string to set request's method.

method
: 'POST',
RequestInit.credentials?: RequestCredentials | undefined

A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.

credentials
: 'include',
RequestInit.headers?: HeadersInit | undefined

A Headers object, an object literal, or an array of two-item arrays to set request's headers.

headers
: { 'x-csrf-token':
const csrf: extension.SiteCookie
csrf
.
value: string
value
, 'content-type': 'application/json' },
RequestInit.body?: BodyInit | null | undefined

A BodyInit object or null to set request's body.

body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
id: number
id
: 42,
score: number
score
: 9 }),
reason?: string | undefined
reason
: 'Save your rating',
})
const response: Response
response
.
Response.ok: boolean

The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.

MDN Reference

ok
// true, once the user allowed both calls
}

The browser’s own matching rules apply (domain, path, secure scheme). httpOnly cookies are readable here. CookieDetails carries no reason, so the consent sheet shows the origin alone. The exact shapes of CookieDetails, RequestHeaderRule and HeaderOperation are under @fkn/lib/extension, and the signature of cloud.fetch under @fkn/lib/cloud/fetch.

The six you are most likely to meet, each linked to its row:

MessageWhat happened
fetch refuses FKN platform domains (<hostname>)The root fetch or cloud.fetch was pointed at an FKN host. See platform hosts.
The FKN WebExtension is not installed, enabled or not exposed on this page.An extension.* call, or the root fetch with credentials: 'include', found no extension and the install card was dismissed.
fetch: refusing to forge request header(s): <names>The content script was handed a header to restore outside origin, referer and cookie, which extension.fetch itself never does.
fetch: a forged Cookie header and credentials:'include' are different identities - send one or the otherYou supplied a cookie header and credentials: 'include' on the same call.
FKN cloud.fetch: no proxy is available (the relay directory could not be read, and no fallback origin is configured)No relay directory could be read and no fallback proxy is configured. Retry after 30,000 ms, when the broker re-reads it.
Permission denied: network.fetchCredentialed (<scope>)The user declined network.fetchCredentialed or network.fetchLocal, or a stored decision refused. error.name is PermissionDeniedError.

The proxy’s own answers are not on that list, because none of them throws. cloud.fetch reads the upstream status off fkn-proxy-status and the upstream headers off fkn-proxy-headers. The proxy’s own refusals carry neither, so one arrives as a plain Response with the proxy’s status, no headers, and a JSON body { "error": "<message>" }. Check response.ok and read error from the body.

A 403 egress refused (non-public target) means the target resolved to a non-public address or did not resolve at all. A 502 upstream fetch failed: <e> means the proxy could not reach or read the upstream. The 400, 413 and 429 rows arrive in the same shape.

Every other message has its row on every error. The rules for matching one are on handling errors.