Skip to content
BasaltSoftware
Bedrock 1.26.60

Site colour

Discord
Esc

    ↑↓ moveEnter openEsc close

    Keyboard shortcuts
    Ctrl K
    Search the whole site, from any page
    /
    Jump to the sidebar search, or search the whole site where there isn't one
    Enter
    Open the top search result
    Esc
    Clear the search, or close this panel
    ?
    Show these shortcuts
    Plugin API

    HTTP requests

    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.

    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);

    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.

    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 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 await a 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.

    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 ws or wss throws a SyntaxError; a host that is not allowed throws a SecurityError.
    • A socket does not reconnect by itself. Open a new one from onclose if you need that, after a delay.
    • When the plugin is reloaded or unloaded, its sockets are closed with code 1001.

    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.

    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.packetReceive sees each packet a client sends, before Basalt handles it. Setting cancel drops the packet.
    • beforeEvents.packetSend sees each packet about to go to a player. Setting cancel leaves it out.
    • The second argument narrows the subscription to monitoredPacketIds, or to everything except ignoredPacketIds.

    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.