Call web APIs, post to webhooks and open WebSockets from a plugin with @minecraft/server-net.
The @minecraft/server-net module lets a plugin talk to the outside world: call a web API, post to a Discord webhook, or hold a WebSocket open to a bot. The HTTP client matches Mojang’s module. WebSockets are Basalt’s own addition.
Add the module to your manifest’s dependencies:
{ "module_name": "@minecraft/server-net", "version": "1.0.0-beta" }On BDS this module needs to be allowed per pack in a permissions file. On Basalt every plugin can use it, unless the owner turns it off; see Settings.
A simple GET
Section titled “A simple GET”http.get returns a promise of an HttpResponse:
import { http } from "@minecraft/server-net";
async function fetchMotd(): Promise<string> { const response = await http.get("https://example.org/motd.txt"); if (response.status !== 200) return "Welcome!"; return response.body;}The promise resolves for any HTTP status, including 404 and 500. Check response.status yourself.
HttpResponse |
|
|---|---|
status |
The HTTP status code |
body |
The body as text |
headers |
The response headers, as HttpHeader objects with key and value |
request |
The request this answers |
To read JSON, parse the body:
const data = JSON.parse(response.body);Building a request
Section titled “Building a request”For anything other than a plain GET, build an HttpRequest and pass it to http.request. The setters chain.
import { world } from "@minecraft/server";import { http, HttpRequest, HttpRequestMethod } from "@minecraft/server-net";
const WEBHOOK = "https://discord.com/api/webhooks/<id>/<token>";
world.afterEvents.chatSend.subscribe(({ sender, message }) => { const request = new HttpRequest(WEBHOOK) .setMethod(HttpRequestMethod.Post) .addHeader("Content-Type", "application/json") .setBody(JSON.stringify({ username: sender.name, content: message })) .setTimeout(5);
http.request(request) .then((response) => { if (response.status >= 300) { console.warn(`Discord answered ${response.status}: ${response.body}`); } }) .catch((error) => console.warn(`Could not reach Discord: ${error.message}`));});HttpRequest |
|
|---|---|
new HttpRequest(uri) |
An http or https address |
setMethod(method) |
HttpRequestMethod.Get (the default), Post, Put, Delete or Head |
addHeader(key, value) |
Adds one header |
setHeaders(headers) |
Replaces the headers with a list of HttpHeader |
setBody(text) |
The request body, as text |
setTimeout(seconds) |
How long to wait for the whole response. The default is 10 seconds. |
Redirects are followed, up to ten in a row.
Errors
Section titled “Errors”When a request gets no response at all, the promise rejects with an HttpError. Its reason says what went wrong:
reason |
Meaning |
|---|---|
"disabled" |
Networking is switched off in basalt.toml. |
"blocked" |
The host, or a host a redirect led to, is not on the allowed list. |
"invalidRequest" |
The address, method or a header is not valid. |
"timeout" |
No full response arrived within the timeout. |
"tooLarge" |
The response body was larger than the configured limit. |
"cancelled" |
http.cancelAll abandoned it. |
"network" |
The connection or TLS failed. |
import { http, HttpError } from "@minecraft/server-net";
async function getJson(uri: string): Promise<unknown> { try { const response = await http.get(uri); if (response.status !== 200) return undefined; return JSON.parse(response.body); } catch (error) { if (error instanceof HttpError) { console.warn(`Request to ${error.uri} failed: ${error.reason}`); } else { console.warn(`Bad JSON from ${uri}`); } return undefined; }}http.cancelAll(reason) abandons every request the plugin has in flight.
Requests do not block the game
Section titled “Requests do not block the game”Requests run in the background. Your plugin keeps running while it waits, and the response is delivered between its other work. You never need to worry about a slow web server stalling the tick.
What this means in practice:
- Do not
awaita request inside a before event expecting to cancel based on the answer. The event will be long over. Decide from data you already have, and refresh that data in the background. - By the time a response arrives, the player you were asking about may have left. Check
player.isValid.
import { server, system, world } from "@minecraft/server";import { http } from "@minecraft/server-net";
// Refresh a ban list every five minutes, and consult the local copy at login.let banned = new Set<string>();
async function refresh(): Promise<void> { try { const response = await http.get("https://example.org/api/bans"); if (response.status === 200) banned = new Set(JSON.parse(response.body)); } catch { // Keep the old list. }}
world.afterEvents.worldLoad.subscribe(() => { void refresh(); system.runInterval(() => void refresh(), 20 * 60 * 5);});
server.beforeEvents.beforeJoin.subscribe((event) => { if (banned.has(event.xuid)) { event.kickMessage = "You are banned from this network."; event.cancel = true; }});When a plugin is reloaded or unloaded, its requests in flight are abandoned.
WebSockets (Basalt)
Section titled “WebSockets (Basalt)”WebSocket works like the browser’s. It is Basalt’s own; BDS has no equivalent.
import { world } from "@minecraft/server";import { WebSocket } from "@minecraft/server-net";
const socket = new WebSocket("wss://bot.example.org/bridge", { headers: { Authorization: "Bearer my-token" },});
socket.onopen = () => socket.send(JSON.stringify({ type: "hello" }));
socket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === "chat") world.sendMessage(`§9[Discord]§r ${data.author}: ${data.text}`);};
socket.onclose = (event) => console.log(`Bridge closed: ${event.code} ${event.reason}`);socket.onerror = (event) => console.warn(`Bridge error: ${event.message}`);
world.afterEvents.chatSend.subscribe(({ sender, message }) => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "chat", author: sender.name, text: message })); }});| Member | |
|---|---|
new WebSocket(url, options?) |
Opens a ws or wss address. Options are headers and protocols. |
send(text) |
Sends a text message |
close(code?, reason?) |
Closes the socket |
readyState |
WebSocket.CONNECTING, OPEN, CLOSING or CLOSED |
onopen, onmessage, onclose, onerror |
Handlers. addEventListener and removeEventListener work too. |
url, protocol |
The address, and the subprotocol the other side chose |
Things to know:
- Messages are text. A binary message arrives as UTF-8 text.
- An address that is not
wsorwssthrows aSyntaxError; a host that is not allowed throws aSecurityError. - A socket does not reconnect by itself. Open a new one from
oncloseif you need that, after a delay. - When the plugin is reloaded or unloaded, its sockets are closed with code 1001.
Settings
Section titled “Settings”The owner controls plugin networking with three settings in basalt.toml:
| Setting | Default | Effect |
|---|---|---|
script_net_enabled |
true |
Off, every request rejects with "disabled" and every WebSocket throws. |
script_net_allowed_hosts |
[] |
The hosts plugins may reach. Empty allows any. |
script_net_max_body_bytes |
8388608 |
The largest response body or WebSocket message accepted (8 MiB). A larger response rejects with "tooLarge"; a larger message closes the socket. |
Entries in the allowed list are host names. "discord.com" matches exactly that host. "*.example.org" matches any host under example.org, but not example.org itself.
script_net_allowed_hosts = ["discord.com", "*.example.org"]The list is checked against the host name in the address and in every redirect, not against the IP address it resolves to.
HTTPS and wss connections trust the certificate authorities the operating system trusts.
Watching packets
Section titled “Watching packets”The same module exposes Basalt’s network traffic as events, as Mojang’s does. Most plugins never need this, but it is useful for debugging and for filtering.
import { beforeEvents, PacketId } from "@minecraft/server-net";
beforeEvents.packetReceive.subscribe( (event) => { console.log(`${event.sender?.name ?? "someone"} sent ${event.packetId} (${event.packetSize} bytes)`); }, { monitoredPacketIds: [PacketId.TextPacket] },);beforeEvents.packetReceivesees each packet a client sends, before Basalt handles it. Settingcanceldrops the packet.beforeEvents.packetSendsees each packet about to go to a player. Settingcancelleaves it out.- The second argument narrows the subscription to
monitoredPacketIds, or to everything exceptignoredPacketIds.
These handlers run while the connection waits, so always narrow them to the packets you need. To watch without making anything wait, use system.afterEvents.packetReceive and system.afterEvents.packetSend from @minecraft/server, which take the same options.
The events give a packet’s id and size, not its contents.
The full list of classes is in @minecraft/server-net.