docs: easy/medium/hard networking recipes (#4040)

* add net category + http request recipes

* add tcp/tls socket recipes

* add complex recipes

* minor refine

---------

Co-authored-by: Reynaldi Chernando <reynaldichernando@gmail.com>
This commit is contained in:
Toshit ChawdaandReynaldi Chernando authored and GitHub committed 2026-10-25 19:22:45 +07:00
1 parent c7cc64e45c
commit ec3ad1cb2f
5 files changed
+571

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ const recipeTags = {
teams: 'Teams',
performance: 'Performance',
'data-modeling': 'Data Modeling',
net: 'Networking',
};
module.exports = recipeTags;
+153
View File
@@ -0,0 +1,153 @@
---
title: Build an RSS Reader
description: Learn how to build an RSS reader with Puter.js, so users can read the latest headlines from any feed.
tags: [net, kv]
order: 70
---
An RSS reader fetches a feed and shows links to its latest articles. Use
[`puter.net.fetch()`](/Networking/fetch/) to read feeds that do not allow
cross-origin requests, then cache the headlines with
[`puter.kv`](/KV/) so reopening the app does not fetch the same feed again.
This example keeps up to 20 headlines for five minutes and lets the user
refresh them sooner. The cache belongs to the signed-in user, inside your
app's key-value store. The first request opens the Puter sign-in window if
the user isn't signed in yet.
## Read the Feed
Fetch the XML, check the HTTP status, and parse it with the browser's
[DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString).
Read the title and link from each RSS `item`. Because DOMParser is a browser
API, this code runs in your app's page:
```js
async function readFeed(feedUrl) {
const response = await puter.net.fetch(feedUrl);
if (!response.ok) {
throw new Error(`Could not load the feed (HTTP ${response.status}).`);
}
const xml = await response.text();
const feed = new DOMParser().parseFromString(xml, 'application/xml');
if (feed.querySelector('parsererror') || feed.documentElement.localName !== 'rss') {
throw new Error('Expected a valid RSS feed.');
}
const articles = [];
for (const item of feed.querySelectorAll('channel > item')) {
const link = item.querySelector('link')?.textContent.trim();
if (!link) continue;
let url;
try {
url = new URL(link, response.url || feedUrl);
} catch {
continue;
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') continue;
articles.push({
title: (item.querySelector('title')?.textContent.trim() || 'Untitled').slice(0, 200),
url: url.href,
});
if (articles.length === 20) break;
}
return articles;
}
```
The function returns plain objects that can go straight into KV. Items
without a valid HTTP or HTTPS link are left out. Relative links are resolved
against the feed's URL.
This parser reads RSS feeds with `channel` and `item` elements. Atom feeds use
a different format and need a different parser.
## Cache the Headlines
Use [`puter.kv.get()`](/KV/get/) to look for a cached result before fetching.
Pass an expiry to [`puter.kv.set()`](/KV/set/) so it goes away after five
minutes. The expiry is a Unix timestamp in seconds:
```js
async function loadFeed(feedUrl, refresh = false) {
const key = `rss:cache:${feedUrl}`;
if (!refresh) {
const cached = await puter.kv.get(key);
if (cached !== null) return cached;
}
const articles = await readFeed(feedUrl);
const inFiveMinutes = Math.floor(Date.now() / 1000) + 5 * 60;
await puter.kv.set(key, articles, inFiveMinutes);
return articles;
}
```
A missing or expired key returns `null`. An empty feed returns an empty
array, which is cached too. Passing `true` for `refresh` fetches a new copy
and replaces the cache after the feed has been read successfully.
Only titles and links are cached, which keeps each value well under the
[KV value size limit](/KV/MAX_VALUE_SIZE/).
## Show the Headlines
Add these elements to a page that loads Puter.js:
```html
<button id="refresh-feed" type="button">Refresh</button>
<p id="feed-status" role="status"></p>
<ul id="headlines"></ul>
```
Put the two functions above and this code in the same script, after the
elements. It loads a BBC News feed on opening and fetches a fresh copy when
the user clicks Refresh:
```js
const feedUrl = 'https://feeds.bbci.co.uk/news/rss.xml';
const refreshButton = document.getElementById('refresh-feed');
const status = document.getElementById('feed-status');
const list = document.getElementById('headlines');
async function showFeed(refresh = false) {
refreshButton.disabled = true;
status.textContent = 'Loading...';
try {
const articles = await loadFeed(feedUrl, refresh);
const rows = articles.map((article) => {
const row = document.createElement('li');
const link = document.createElement('a');
link.textContent = article.title;
link.href = article.url;
row.append(link);
return row;
});
list.replaceChildren(...rows);
status.textContent = articles.length ? 'Headlines loaded.' : 'No articles in this feed.';
} catch (error) {
const message = error?.message ?? error?.error?.message ?? error;
status.textContent = `Could not load the headlines: ${message}`;
} finally {
refreshButton.disabled = false;
}
}
refreshButton.addEventListener('click', () => showFeed(true));
showFeed();
```
Titles are assigned through `textContent`, so markup in a feed stays text.
If a refresh fails, the current headlines stay on the page and the status
shows the error.
## Notes
- To keep full articles or other large feed data, save it as files instead.
See [store files](/recipes/store-files/).
@@ -0,0 +1,153 @@
---
title: Check Website Uptime
description: Learn how to check whether a website is up with Puter.js and keep a history of recent checks for each user.
tags: [net, kv]
order: 71
---
To see whether a website is responding, make a request with
[`puter.net.fetch()`](/Networking/fetch/) and save the result with
[`puter.kv.set()`](/KV/set/). This gives your app a history of successful
checks, HTTP errors and connection failures.
This example checks a site when the user clicks a button, keeps each result
for seven days, and shows the latest 20 checks. Each user's history lives in
their own account. The first check opens the Puter sign-in window if the user
isn't signed in yet.
## Check the Site
Use a `HEAD` request to read the site's status without downloading its page.
Check `response.ok` to tell whether it returned a status from `200` to `299`,
and catch request failures so they can be saved too:
```js
async function checkWebsite(url) {
const started = performance.now();
const result = {
at: Date.now(),
up: false,
status: null,
durationMs: 0,
error: null,
};
try {
const response = await puter.net.fetch(url, { method: 'HEAD' });
result.up = response.ok;
result.status = response.status;
} catch (error) {
result.error = String(error?.message ?? error?.error?.message ?? error);
}
result.durationMs = Math.round(performance.now() - started);
const prefix = `uptime:${encodeURIComponent(url)}:`;
const key = `${prefix}${new Date(result.at).toISOString()}:${crypto.randomUUID()}`;
const inSevenDays = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60;
await puter.kv.set(key, result, inSevenDays);
return result;
}
```
An HTTP error has a status, such as `503`. A request that fails before getting
a response has `status: null` and an error message. A failed KV write still
rejects, so the app can tell the user when a result couldn't be saved.
This example treats every status outside `200` to `299` as down. Change that
rule if your endpoint uses another status for a successful check. Use an
endpoint that accepts `HEAD`, because a site that rejects it can return `405`
even when its pages work. For a `GET` check, cancel the response body with
`await response.body?.cancel()` after reading the status.
The elapsed time includes connection setup and the network path from the
user's browser, so each result is one sample of the site's availability.
Each check has its own key. The timestamp sorts checks by time, and the UUID
keeps two checks made in the same millisecond from replacing each other.
The third argument to [`puter.kv.set()`](/KV/set/) is an expiry timestamp in
seconds.
## Read Recent Checks
List the site's keys with [`puter.kv.list()`](/KV/list/). Pass `reverse: true`
to put the newest first and `limit: 20` to read one page of history:
```js
async function getHistory(url) {
const page = await puter.kv.list({
pattern: `uptime:${encodeURIComponent(url)}:*`,
returnValues: true,
reverse: true,
limit: 20,
fetchUntilFull: true,
});
return page.items.map((entry) => entry.value);
}
```
With `limit`, [`puter.kv.list()`](/KV/list/) returns a page object whose
`items` hold the key-value pairs. The `fetchUntilFull` option fills the page
when possible, even if expired keys are skipped. A site with no saved checks
returns an empty list.
To show older results, keep the page's `cursor` and request the next page, as
in [store a large collection](/recipes/store-large-collection/).
## Show the History
Add a button, a status message and a place for the history to a page that
loads Puter.js:
```html
<button id="check-site" type="button">Check now</button>
<p id="check-status" role="status"></p>
<pre id="check-history"></pre>
```
Put the two functions above and this code in the same script, after the
elements. Change `targetUrl` to the site or health endpoint you want to check:
```js
const targetUrl = 'https://example.com/';
const checkButton = document.getElementById('check-site');
const status = document.getElementById('check-status');
const history = document.getElementById('check-history');
async function showHistory() {
const checks = await getHistory(targetUrl);
history.textContent = checks.length ? checks.map((check) => {
const time = new Date(check.at).toLocaleString();
const outcome = check.up ? 'Up' : 'Down';
const detail = check.status === null ? check.error : `HTTP ${check.status}`;
return `${time} - ${outcome} - ${detail} - ${check.durationMs} ms`;
}).join('\n') : 'No checks yet.';
}
checkButton.addEventListener('click', async () => {
checkButton.disabled = true;
status.textContent = 'Checking...';
try {
const result = await checkWebsite(targetUrl);
status.textContent = result.up ? 'The site responded successfully.' : 'The check failed.';
await showHistory();
} catch (error) {
const message = error?.message ?? error?.error?.message ?? error;
status.textContent = `Could not save or load the results: ${message}`;
} finally {
checkButton.disabled = false;
}
});
showHistory().catch((error) => {
const message = error?.message ?? error?.error?.message ?? error;
status.textContent = `Could not load the history: ${message}`;
});
```
The button stays disabled until the check and history refresh finish. A
connection failure is saved as a failed check, and its message appears in
the history. Checks run only when the user clicks the button, so closing the
page stops them.
+152
View File
@@ -0,0 +1,152 @@
---
title: Connect to a Server
description: Learn how to open TCP and TLS connections from the browser with Puter.js, so your app can talk to any server on any port.
tags: [net]
order: 66
---
To talk to a server over a TCP connection, use the
[`puter.net.Socket()`](/Networking/Socket/) constructor. For an encrypted
connection, use [`puter.net.tls.TLSSocket()`](/Networking/TLSSocket/).
Both take a hostname and a port, then send and receive data through events.
Browsers can't open raw TCP connections on their own, so these sockets give
your app's page direct access to servers such as mail, database and WHOIS
servers. The first connection opens the Puter sign-in window if the user
isn't signed in yet.
For most HTTP requests, [use `puter.net.fetch()`](/recipes/make-http-requests/).
A socket is useful when you need to send the protocol's bytes yourself.
## Send an HTTP Request
To connect to an HTTP server, pass its hostname and port `80`. Wait for the
`open` event, then send the request with the
[`write()`](/Networking/Socket/) method:
```js
const socket = new puter.net.Socket('example.com', 80);
const decoder = new TextDecoder();
socket.on('open', () => {
socket.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n');
});
socket.on('data', (data) => {
console.log(decoder.decode(data, { stream: true }));
});
socket.on('error', (error) => {
console.error('Connection failed:', error.message);
});
socket.on('close', () => {
const remaining = decoder.decode();
if (remaining) console.log(remaining);
});
```
The server sends bytes in `data` events. This prints the HTTP response,
including its headers. The `Connection: close` header asks the server to close
the connection after sending it. Each `\r\n` ends a line, and the empty line
ends the request headers.
Passing `{ stream: true }` to `TextDecoder` keeps a character that's split
across two chunks intact. The last `decode()` call in the `close` handler
flushes any remaining text.
## Use TLS
To make the same request over an encrypted connection, use
[`puter.net.tls.TLSSocket()`](/Networking/TLSSocket/) and port `443`.
Its connection events are `tlsopen`, `tlsdata` and `tlsclose`. Errors still
use `error`:
```js
const socket = new puter.net.tls.TLSSocket('example.com', 443);
const decoder = new TextDecoder();
socket.on('tlsopen', () => {
socket.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n');
});
socket.on('tlsdata', (data) => {
console.log(decoder.decode(data, { stream: true }));
});
socket.on('error', (error) => {
console.error('Connection failed:', error.message);
});
socket.on('tlsclose', () => {
const remaining = decoder.decode();
if (remaining) console.log(remaining);
});
```
Use a TLS socket on a port where the server accepts TLS connections.
## Send Bytes
The [`write()`](/Networking/Socket/) method also accepts a `Uint8Array` or an
`ArrayBuffer`.
Use `TextEncoder` to turn text into UTF-8 bytes. This example asks the
[IANA WHOIS server](https://www.iana.org/whois) for information about `.com`:
```js
const socket = new puter.net.Socket('whois.iana.org', 43);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let reply = '';
socket.on('open', () => {
socket.write(encoder.encode('com\r\n'));
});
socket.on('data', (data) => {
reply += decoder.decode(data, { stream: true });
});
socket.on('error', (error) => {
console.error('WHOIS request failed:', error.message);
});
socket.on('close', (hadError) => {
reply += decoder.decode();
if (!hadError) console.log(reply);
});
```
[WHOIS](https://www.rfc-editor.org/rfc/rfc3912) uses port `43`, takes a query
ending in `\r\n`, and closes the connection when the reply is complete.
The example collects all the chunks before printing it.
A `data` event can contain part of a message or several messages, so use the
protocol's rules to decide when a reply is complete. For a binary protocol,
work with the received `Uint8Array` directly and decode only the bytes that
the protocol defines as text.
## Handle Errors and Close
A socket connects after you create it. Listen for `error` to handle a failed
connection, and for `close` to know when it ends. The `hadError` argument is
`true` when the connection closes because of an error:
```js
const socket = new puter.net.Socket('does-not-exist.invalid', 80);
socket.on('error', (error) => {
console.error('Connection failed:', error.message);
});
socket.on('close', (hadError) => {
console.log('Connection closed. Had an error:', hadError);
});
```
To close a connection yourself, call the [`close()`](/Networking/Socket/)
method on the socket you opened:
```js
socket.close();
```
+112
View File
@@ -0,0 +1,112 @@
---
title: Make HTTP Requests
description: Learn how to make HTTP requests from the browser with Puter.js, so your app can call any API or website, even ones that block cross-origin requests.
tags: [net]
order: 65
---
The browser's `fetch()` blocks a response when the server doesn't allow
cross-origin requests, so many APIs and websites can't be called from your
app's page. The [`puter.net.fetch()`](/Networking/fetch/) method makes the
same HTTP and HTTPS requests without that restriction. It takes the same
arguments as `fetch()` and returns a `Response`.
The first request opens the Puter sign-in window if the user isn't signed in
yet.
## Fetch a JSON API
To fetch JSON, pass the API's URL, then read the response with `json()`:
```js
const response = await puter.net.fetch('https://httpbin.org/get');
const data = await response.json();
console.log(data);
```
The method returns a `Response` object. For a page or a plain text file, use
`response.text()` instead.
## Send JSON
To send JSON, pass `method`, `headers` and `body` in the second argument.
Use `JSON.stringify()` to turn the object into a string and set `Content-Type`
so the server knows how to read it. Custom headers go in the same object:
```js
const response = await puter.net.fetch('https://httpbin.org/post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Custom-Header': 'custom-value',
},
body: JSON.stringify({ message: 'Hello' }),
});
const data = await response.json();
console.log(data);
```
This example uses httpbin, which sends back the JSON and headers it received.
The other request options are listed on
[`puter.net.fetch()`](/Networking/fetch/).
## Read a Response in Chunks
To process a large response as it arrives, read from `response.body` instead
of waiting for `text()` or `json()` to collect the whole body:
```js
const response = await puter.net.fetch('https://httpbin.org/stream-bytes/102400');
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value);
}
} finally {
reader.releaseLock();
}
```
Each `value` is a `Uint8Array` of bytes. Chunk sizes can vary. Some responses,
such as those to a `HEAD` request, have no body, so `response.body` is `null`.
## Handle a Failed Request
A request can fail before you get a response, for example when the host cannot
be reached. Catch the error with `try`/`catch`. HTTP errors such as `404` or
`500` still return a response, so check `response.ok` before reading the body:
```js
try {
const response = await puter.net.fetch('https://httpbin.org/get');
if (!response.ok) {
throw new Error(`Request failed (HTTP ${response.status}).`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Could not load the data:', error?.message ?? error);
}
```
The `ok` property is `true` for status codes from `200` to `299`. The same
catch also handles a response that can't be read as JSON.
To try the HTTP error path, replace the URL with
`https://httpbin.org/status/404`. To try a host that does not exist, use
`https://does-not-exist.invalid/`.
## Notes
- For an API that needs a secret key, call it from a worker so the key stays
out of your app's page. See [keep an API key
secret](/recipes/keep-api-key-secret/).