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:
credentials: 'include' on the init pins the extension. The call never falls back to the cloud.
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'.
The desktop backend is asked and always answers no.
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.
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' })
constme:Response
me.
Response.url: string
The url read-only property of the Response interface contains the URL of the response.
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.
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:
url// '', the broker rebuilt the Response on its side
constresponse: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.
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:
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 (!
constresponse: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.
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:
the 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:
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',
})
constresponse:Response
response.
Response.status: number
The status read-only property of the Response interface contains the HTTP status codes of the response.
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 status0, 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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
promptInstall('Sync your library from your own account') // from your own button
constinstalled: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
constjar:"session=abc123; theme=dark"
jar='session=abc123; theme=dark'// one string, never a repeated cookie header
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.
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:
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.
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:
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
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:
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':
constcsrf: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:
varJSON:JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@param ― value A JavaScript value, usually an object or array, to be converted.
@param ― replacer A function that transforms the results.
@param ― space 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',
})
constresponse: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.
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.