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 Method | SDK Method | Description |
|---|---|---|
network.fetch | network.fetch() | Execute an HTTP request (text or base64 response) |
network.downloadToFile | network.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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | -- | The URL to fetch. |
options.method | string | No | "GET" | HTTP method: GET, POST, PUT, DELETE, PATCH. Case-insensitive. |
options.headers | Record<string, string> | No | {} | Request headers to include. |
options.body | string | object | null | No | null | Request 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:
| Field | Type | Description |
|---|---|---|
status | number | HTTP status code (e.g., 200, 404, 500). Defaults to 0 if unavailable. |
statusText | string | HTTP reason phrase (e.g., "OK", "Not Found"). Defaults to "" if unavailable. |
ok | boolean | true if status is in the range 200–299, false otherwise. |
headers | Record<string, string> | Response headers. Multi-value headers are joined with ", ". |
body | string | Response 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 theContent-Typeheader. If the response is JSON, callJSON.parse()on thebodyyourself. - HTTP errors are never thrown. Non-2xx status codes (e.g.,
404,500) are returned as normal results with the appropriatestatusandstatusText. 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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | -- | The URL to download. Same security rules as network.fetch. |
destUri | string | Yes | -- | Destination file URI (file:// or content://). Parent directories are created if missing; an existing file is overwritten. |
options.headers | Record<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
| Field | Type | Description |
|---|---|---|
ok | boolean | true when status is 200–299 and the file write succeeded. |
status | number | HTTP status code from the request. |
statusText | string | HTTP reason phrase. |
size | number | Number of bytes written (0 when ok is false). |
destUri | string | Echoes 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: falseandsize: 0. The file is not created. - File-system permission applies to
destUri. IfdestUriis 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.fetchapply tourl(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:
| Port | Purpose |
|---|---|
4820 | Terminal server port |
3200 | Internal app port |
Blocked URL Patterns
Requests to localhost addresses on blocked ports are rejected. The following hostnames are treated as localhost:
localhost127.0.0.1::10.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:
| Condition | Error 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);
}
});