mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-05 05:24:13 +00:00
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:
1 parent
c7cc64e45c
commit
ec3ad1cb2f
5 files changed
+571
No files matched your search
@@ -18,6 +18,7 @@ const recipeTags = {
|
||||
teams: 'Teams',
|
||||
performance: 'Performance',
|
||||
'data-modeling': 'Data Modeling',
|
||||
net: 'Networking',
|
||||
};
|
||||
|
||||
module.exports = recipeTags;
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
```
|
||||
|
||||
@@ -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/).
|
||||
Reference in new issue
Block a user