Skip to main content

Network (HTTP Fetch) API

The Network API allows extensions to make HTTP requests to external services. It provides a fetch-style interface, giving extensions the ability to interact with REST APIs, retrieve remote data, and submit form data -- all without requiring any special permission in the extension manifest.

Overview

The Network API is part of the network namespace in the extension SDK. It exposes the following RPC methods:

RPC MethodSDK MethodDescription
network.fetchnetwork.fetch()Execute an HTTP request (text or base64 response)
network.downloadToFilenetwork.downloadToFile()Stream a URL directly to a file on disk

No permission declaration is required in the extension manifest. Any extension can use the Network API immediately. (network.downloadToFile writes to disk, so the destination URI follows the same file-system access rules as workspace.fs.write — see File System API › Permissions.)

network.fetch

Performs an HTTP request and returns the response.

SDK Signature

const response = await editor.network.fetch(url, options);

Parameters

ParameterTypeRequiredDefaultDescription
urlstringYes--The URL to fetch.
options.methodstringNo"GET"HTTP method: GET, POST, PUT, DELETE, PATCH. Case-insensitive.
options.headersRecord<string, string>No{}Request headers to include.
options.bodystring | object | nullNonullRequest body. Relevant for POST, PUT, and PATCH.
options.responseType"text" | "base64"No"text"How the response body is encoded. See Binary responses.

RPC-Level Parameters

When calling the raw RPC method network.fetch, the parameters map is:

{
"url": "https://api.example.com/data",
"method": "GET",
"headers": { "Authorization": "Bearer token123" },
"body": null,
"responseType": "text"
}

The url parameter is required and must be a string. The method parameter defaults to "GET" if omitted. The responseType parameter defaults to "text".

Response Format

The response is an object with the following fields:

FieldTypeDescription
statusnumberHTTP status code (e.g., 200, 404, 500). Defaults to 0 if unavailable.
statusTextstringHTTP reason phrase (e.g., "OK", "Not Found"). Defaults to "" if unavailable.
okbooleantrue if status is in the range 200–299, false otherwise.
headersRecord<string, string>Response headers. Multi-value headers are joined with ", ".
bodystringResponse body. Plain text when responseType is "text", or a base64-encoded string when responseType is "base64".
responseType"text" | "base64"Echoes the encoding actually used for body.

Example response:

{
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=43200"
},
"body": "{\"id\":1,\"title\":\"Example\"}"
}

Key Behaviors

  • Response type defaults to plain text. Unless responseType: "base64" is set, the body is returned as a raw UTF-8 string regardless of the Content-Type header. If the response is JSON, call JSON.parse() on the body yourself.
  • HTTP errors are never thrown. Non-2xx status codes (e.g., 404, 500) are returned as normal results with the appropriate status and statusText. This lets your extension handle error responses gracefully rather than catching exceptions.
  • Network-level errors are thrown. Failures such as DNS resolution errors, connection timeouts, or unreachable hosts will throw an exception with a message like "Network request failed: <details>".
  • Multi-value headers are joined. If the server sends multiple values for the same header, they are combined into a single comma-separated string (e.g., "value1, value2").
  • RPC timeout is 30 seconds. If the request does not complete within 30 seconds, the RPC call will time out and return an "RPC timeout" error.

Binary responses

Pass responseType: "base64" to receive the body as a base64-encoded string. This is the right choice for binary content (images, fonts, archives, PDFs) where forcing the bytes through UTF-8 string decoding would corrupt them.

const res = await editor.network.fetch("https://example.com/logo.png", {
responseType: "base64",
});

if (res.ok) {
// res.body is the base64-encoded image. Decode it via atob() in JS,
// or hand it directly to workspace.fs.writeBase64() to save it as
// a real binary file on disk.
await editor.workspace.fs.writeBase64(destUri, res.body);
}

When responseType is "base64", the bytes still travel through the JS runtime as a string. This costs roughly 33% extra memory compared to the original byte size. For large downloads where the extension does not need to inspect the contents, prefer network.downloadToFile instead — it streams bytes from Dart straight to disk and never enters the JS runtime.

network.downloadToFile

Download a URL directly to a file on disk. The bytes are fetched and written entirely on the host (Dart) side and never enter the JS runtime, making this the right choice for large binary downloads such as images, fonts, videos, and archives.

SDK Signature

const result = await editor.network.downloadToFile(url, destUri, options);

Parameters

ParameterTypeRequiredDefaultDescription
urlstringYes--The URL to download. Same security rules as network.fetch.
destUristringYes--Destination file URI (file:// or content://). Parent directories are created if missing; an existing file is overwritten.
options.headersRecord<string, string>No{}Request headers to include with the GET request.

RPC-Level Parameters

{
"url": "https://example.com/big.zip",
"destUri": "file:///path/to/project/downloads/big.zip",
"headers": {}
}

Response Format

FieldTypeDescription
okbooleantrue when status is 200–299 and the file write succeeded.
statusnumberHTTP status code from the request.
statusTextstringHTTP reason phrase.
sizenumberNumber of bytes written (0 when ok is false).
destUristringEchoes the destination URI on success.

Example success:

{
"ok": true,
"status": 200,
"statusText": "OK",
"size": 152834,
"destUri": "file:///.../downloads/big.zip"
}

Key Behaviors

  • Bytes never enter JS. Memory usage in the extension runtime is constant regardless of file size — only the small response object is returned.
  • Parent directories are created automatically before writing.
  • Existing files are overwritten. If a directory already exists at destUri (e.g. left over from a previous run), it is removed first so the file write can succeed.
  • HTTP errors do not throw. A non-2xx response sets ok: false and size: 0. The file is not created.
  • File-system permission applies to destUri. If destUri is outside the currently open project, the extension's data directory, or a project the extension created, the user is prompted as with any other write — see File System API › Permissions.
  • Same security rules as network.fetch apply to url (blocked ports, scheme validation, etc.).

Example

editor.commands.registerCommand("myExt.downloadLogo", async () => {
const projectRoot = await editor.workspace.getProjectRoot();
if (!projectRoot) return;

const dest = projectRoot + "/assets/logo.png";
const result = await editor.network.downloadToFile(
"https://example.com/logo.png",
dest,
);

if (result.ok) {
await editor.window.showToast(`Saved ${result.size} bytes to logo.png`);
} else {
await editor.window.showToast(`Download failed: HTTP ${result.status}`);
}
});

Security

The Network API enforces several security restrictions to prevent extensions from accessing internal app services.

Blocked Ports

The following ports are reserved for internal app use and cannot be accessed by extensions:

PortPurpose
4820Terminal server port
3200Internal app port

Blocked URL Patterns

Requests to localhost addresses on blocked ports are rejected. The following hostnames are treated as localhost:

  • localhost
  • 127.0.0.1
  • ::1
  • 0.0.0.0

A request to any of these hosts on a blocked port (e.g., http://localhost:4820/... or http://127.0.0.1:3200/...) will throw an exception:

Access to localhost:4820 is not allowed for extensions

Unparseable URLs

If the provided URL cannot be parsed, it is rejected defensively with the error:

Invalid URL: <url>

Allowed Requests

  • Requests to any external host (e.g., https://api.github.com) are allowed.
  • Requests to localhost on non-blocked ports (e.g., http://localhost:8080) are allowed. This enables extensions to communicate with local development servers.

Note on file:// URLs

The file:// URL scheme is not explicitly blocked at the HTTP fetch level, but the underlying HTTP client does not support file:// URLs, so such requests will fail with a network error.

Error Handling

Errors from network.fetch fall into two categories:

1. HTTP Error Responses (Non-Throwing)

Non-2xx responses are returned as normal results. Check the status field:

const res = await editor.network.fetch("https://api.example.com/missing");

if (res.status === 404) {
console.log("Resource not found");
} else if (res.status >= 500) {
console.log("Server error:", res.statusText);
}

2. Exceptions (Throwing)

The following conditions throw an exception that should be caught with try/catch:

ConditionError Message Pattern
Blocked port"Access to localhost:<port> is not allowed for extensions"
Invalid/unparseable URL"Invalid URL: <url>"
DNS failure / no connection"Network request failed: <details>"
Other transport error"Failed to execute fetch for <url>: <details>"
RPC timeout (30 seconds)"RPC timeout"
try {
const res = await editor.network.fetch("https://unreachable.example.com");
// Use res.status, res.body, etc.
} catch (err) {
await editor.window.showToast("Request failed: " + err.message);
}

Examples

GET Request

Fetch data from a JSON API:

const res = await editor.network.fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);

if (res.status === 200) {
const post = JSON.parse(res.body);
console.log(post.title);
}

POST Request with JSON Body

Send JSON data to an API:

const res = await editor.network.fetch(
"https://jsonplaceholder.typicode.com/posts",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "New Post",
body: "Content goes here",
userId: 1,
}),
}
);

if (res.status === 201) {
const created = JSON.parse(res.body);
await editor.window.showToast("Created post #" + created.id);
}

PUT Request

Update an existing resource:

const res = await editor.network.fetch(
"https://jsonplaceholder.typicode.com/posts/1",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: 1,
title: "Updated Title",
body: "Updated content",
userId: 1,
}),
}
);

DELETE Request

Delete a resource:

const res = await editor.network.fetch(
"https://jsonplaceholder.typicode.com/posts/1",
{ method: "DELETE" }
);

if (res.status === 200) {
await editor.window.showToast("Post deleted");
}

Request with Custom Headers

Include authentication or other custom headers:

const apiKey = "your-api-key";

const res = await editor.network.fetch(
"https://api.example.com/protected/data",
{
headers: {
"Authorization": "Bearer " + apiKey,
"Accept": "application/json",
},
}
);

Handling All Response Fields

Inspect the full response including headers:

const res = await editor.network.fetch("https://api.example.com/data");

console.log("Status:", res.status, res.statusText);
console.log("Content-Type:", res.headers["content-type"]);
console.log("Body:", res.body);

Displaying Results in a Popup

Combine the Network API with the Window API to show results to the user:

editor.commands.registerCommand("myExt.fetchAndShow", async () => {
try {
await editor.window.showToast("Fetching data...");

const res = await editor.network.fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);

let body;
try {
body = JSON.stringify(JSON.parse(res.body), null, 2);
} catch {
body = res.body;
}

await editor.window.showPopup({
title: "Fetch Result",
contentType: "markdown",
markdown: [
"## Response",
"",
"**Status:** " + res.status + " " + res.statusText,
"",
"**Body:**",
"```json",
body.substring(0, 1000),
"```",
].join("\n"),
});
} catch (err) {
await editor.window.showToast("Error: " + err.message);
}
});