The fetch API
fetch makes HTTP requests from your scripts. Use it for REST APIs, webhooks, and anything else that lives
outside the server.
fetchis synchronous and blocks until the response arrives. Always call it insideScheduler.runAsync(). Calling it on the main thread freezes your server for as long as the remote service takes to answer, up to the 30 second timeout.
It is deliberately not the browser fetch. There are no promises, no await, and the response object
has its own small API.
fetch(url, options?): Response
| Option | Description |
|---|---|
method |
GET, POST, PUT, PATCH, DELETE. Defaults to GET |
headers |
Object of header name to value |
postBody |
Request body, sent with POST, PUT and PATCH |
Scheduler.runAsync(() => {
const response = fetch('https://gateway.openaudiomc.net/');
if (!response.isSuccessful()) {
warn(`Request failed with status ${response.getStatusCode()}`);
return;
}
const data = response.asObject();
log(`MOTD: ${data.motd}`);
});
The Response object
| Method | Returns |
|---|---|
asString() |
The raw response body |
asObject() |
The body parsed as JSON, or null if parsing failed |
getStatusCode() |
The HTTP status code, or -1 on a network failure |
isSuccessful() |
true when the status is between 1 and 399 |
isFailed() |
true when the status is negative or 400 and above |
A status code of -1 means the request never completed: DNS failure, connection refused, or a timeout.
That distinction is worth surfacing in your logs:
if (response.isFailed()) {
if (response.getStatusCode() < 0) {
error('Network error or timeout');
} else {
error(`Request failed: HTTP ${response.getStatusCode()}`);
}
}
Error responses still have a readable body, so asString() on a 4xx gives you the API’s explanation.
Sending data
Scheduler.runAsync(() => {
const payload = {
server: 'PSCraft',
event: 'player_join',
player: playerName,
};
const response = fetch('https://api.example.com/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`,
},
postBody: JSON.stringify(payload),
});
if (!response.isSuccessful()) {
warn(`Event dispatch failed: ${response.getStatusCode()}`);
}
});
PUT, PATCH and DELETE work the same way. Only POST, PUT and PATCH send postBody.
Coming back to the main thread
The response arrives on the async thread. Anything touching game state has to hop back:
Scheduler.runAsync(() => {
const response = fetch(`https://api.mojang.com/users/profiles/minecraft/${name}`);
if (!response.isSuccessful()) return;
const profile = response.asObject();
Scheduler.runSync(() => {
sender.sendRichMessage(`<green>UUID: <white>${profile.id}`);
});
});
Parsing responses
asObject() catches its own parse errors and returns null rather than throwing, so check the result:
const data = response.asObject();
if (data === null) {
error('Response was not valid JSON');
log(`Raw body: ${response.asString()}`);
return;
}
If you need finer control, take asString() and use JSON.parse in a try/catch.
Timeouts
Both connection and read timeouts are fixed at 30 seconds. A request that exceeds them fails with
status -1. There is no way to raise this from a script, so if you are talking to a slow service, do the
work on a timer rather than in response to a player action.
Rate limiting
There is no built-in rate limiting. If you are calling an API on a schedule or per player, protect it yourself:
const lastCall = new Map();
const COOLDOWN_MS = 60_000;
function fetchProfileCached(uuid, callback) {
const now = Date.now();
if (lastCall.has(uuid) && now - lastCall.get(uuid) < COOLDOWN_MS) {
return callback(null);
}
lastCall.set(uuid, now);
Scheduler.runAsync(() => {
const response = fetch(`https://api.example.com/profile/${uuid}`);
callback(response.isSuccessful() ? response.asObject() : null);
});
}
Best practices
Do
- Wrap every call in
Scheduler.runAsync() - Check
isSuccessful()before reading the body - Handle
asObject()returningnull - Keep tokens in a
DataFileor an environment variable, not in the script - Set
Content-Typewhen you send a body
Do not
- Call
fetchon the main thread - Call it per tick, or once per player in a loop over everyone online
- Assume the response is valid JSON
- Hardcode credentials