Editor embedding
Errors and connection limits
Embed error codes, SSRF rules, browser-side behaviour when the connection cap is reached, and time and size limits.
On this page
Every error reaches your page through onError as {code, message, detail}: code is stable for branching, message is a Vietnamese sentence you can show. Reaching the connection cap is not an error and goes through onLimitReached.
Server-side errors#
| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | invalid_config | Missing or wrong field; detail.errors = [{path, message}]. | Fix the config; do not retry. |
| 401 | token_required | The server requires a JWT but the config has none. | Sign the config on your server. |
| 401 | invalid_token | Bad signature, not HS256, missing exp, exp more than 24 hours away. | Check the secret and algorithm. |
| 401 | token_expired | Past exp. | Get a new token and reopen. |
| 401 | embed_auth_not_configured | No secret on the server and unsigned mode is off. | Admin sets O3O_EMBED_JWT_SECRET. |
| 401 | invalid_session_token | Authorization does not match the session. | Create a new session. |
| 403 | origin_not_allowed | Origin is not in O3O_EMBED_ALLOWED_ORIGINS. | Add the page origin. |
| 403 | invalid_signature | Wrong signature on a download URL. | Use the URL exactly as returned. |
| 404 | not_found | Session or document does not exist; or embedding is disabled. | Check O3O_EMBED_ENABLED. |
| 410 | link_expired, version_gone | Download URL expired; version was cleaned up. | Download sooner, within the retention time. |
| 413 | file_too_large | Above O3O_EMBED_MAX_FILE_MB (100 MB by default). | Do not retry. |
| 415 | unsupported_file_type | fileType outside the list or not openable by the editor server. | Do not retry. |
| 422 | url_not_allowed | SSRF rule violation. | Use a public URL or configure O3O_FETCH_ALLOW_HOSTS. |
| 422 | download_failed | Could not download document.url. | Check the file server; retry is possible. |
| 503 | editor_unavailable | The gate cannot read the editor server's discovery. | Retry later; alert the admin. |
Browser-only errors#
| code | When |
|---|---|
network_error | The O3O server cannot be reached. |
load_failed | The editor frame reports a load failure, or the document does not show within 120 seconds. |
save_failed | The editor frame reports a failed save; detail holds the raw message. |
save_timeout | save() took more than 30 seconds. |
SSRF rules#
Applied to every URL the server calls itself (document.url, callbackUrl): http/https only, no user:pass@, at most 2048 characters; every resolved address must be public (private, loopback, link-local and multicast ranges are blocked), and the gate connects straight to the checked IP address without resolving again (DNS rebinding protection); redirects are not followed automatically (downloads handle up to 3 and re-check each hop, callbacks treat a redirect as a failure); only status 200 counts as a successful download. The one deliberate exception: O3O_FETCH_ALLOW_HOSTS.
When the connection cap is reached#
- The page asks for
mode: "edit"whilecurrent + pending >= limit: the gate still creates the session, inview, returnslimited: true, andapi.jsfiresonLimitReached. api.jsinserts a light-yellow banner with a dismiss button above the frame, showing the server'smessageverbatim. Turn it off withui.limitBanner: false.- Open editing sessions are never cut and saves are never blocked.
- Reopening the document you just left within 120 seconds is allowed even at the cap (brief network loss, page reload).
- The community edition cap is 50; the enterprise edition follows the connections purchased. See limits and plans.
{
"session_id": "ses_0c4e9a1b2d3f4a5b6c7d8e9f",
"key": "contract-42-v7",
"doc_id": "doc_9835f61009fc659dfedab8696f2d2665",
"mode": "view",
"requested_mode": "edit",
"limited": true,
"limit": {
"limit": 50,
"current": 50,
"edition": "community"
},
"message": "Hệ thống đã đạt giới hạn 50 phiên soạn thảo đồng thời của bản cộng đồng. Tài liệu của bạn vẫn an toàn. Bạn có thể mở ở chế độ chỉ đọc hoặc thử lại sau ít phút.",
"editor_url": "/browser/825c9caa93/cool.html?WOPISrc=http%3A%2F%2Fo3o-gate%3A8070%2Fo3o%2Fwopi%2Ffiles%2Fdoc_9835f61009fc659dfedab8696f2d2665&lang=vi&permission=readonly",
"form": {
"access_token": "kq3…",
"access_token_ttl": 1790043200000,
"ui_defaults": "UIMode=notebookbar"
},
"version": 0,
"expires_at": "2026-09-21T22:00:00Z"
}<div id="banner" hidden style="background: #fff4d6; padding: 8px 12px"></div>
<div id="o3o-editor" style="height: 90vh"></div>
<script src="http://localhost:8080/o3o/api.js"></script>
<script>
let editor;
let retries = 0;
async function openEditor() {
// Your server returns a signed config (see Signing the config with JWT)
const config = await (await fetch("/o3o/config?doc=42")).json();
editor = new O3O.Editor("o3o-editor", {
...config,
ui: { ...(config.ui || {}), limitBanner: false }, // draw our own banner
events: { onDocumentLoaded: () => { retries = 0; }, onError, onLimitReached }
});
}
function onError(e) {
switch (e.code) {
case "token_expired": // token expired: fetch a new config and reopen
case "network_error": // O3O unreachable: retry with growing delay
case "load_failed":
if (retries++ < 3) {
editor.destroy();
setTimeout(openEditor, 2000 * retries);
return;
}
break;
case "save_failed":
case "save_timeout": // the document is still open; ask the user to save again
return showBanner("Could not save. Please save again.");
case "url_not_allowed":
case "download_failed":
case "file_too_large":
case "unsupported_file_type": // file or URL problem: retrying will not help
return showBanner(e.message);
}
showBanner(e.message);
console.error(e.code, e.detail);
}
function onLimitReached(e) {
// Not an error: the document is open read-only
showBanner(e.message, "Try editing again", () => editor.setReadOnly(false));
}
function showBanner(text, actionLabel, action) {
const el = document.getElementById("banner");
el.textContent = text;
if (actionLabel) {
const b = document.createElement("button");
b.textContent = actionLabel;
b.onclick = action;
el.append(" ", b);
}
el.hidden = false;
}
openEditor();
</script>Other limits#
| Limit | Default | Variable |
|---|---|---|
| Embedded file size (download and save) | 100 MB | O3O_EMBED_MAX_FILE_MB |
Session access_token lifetime | 720 minutes | O3O_EMBED_SESSION_TTL_MINUTES |
| Created session the editor never loads | cancelled after 300 seconds | O3O_EMBED_PENDING_TTL_SECONDS |
| Working copy kept after closing | 24 hours | O3O_EMBED_RETAIN_HOURS |
| Waiting for the document to show | 120 seconds, then load_failed | fixed |
save() | 30 seconds, then save_timeout | fixed |
close() waiting for a save | 10 seconds | fixed |
Embedding page on another origin#
A page on localhost (any port) can embed without configuration. A page on another origin needs two things: add the origin to O3O_EMBED_ALLOWED_ORIGINS (CORS for /o3o/embed/*), and set O3O_ONLINE_FRAME_ANCESTORS (a single value without spaces, e.g. https://app.example.com or https://*.example.com) so the browser allows framing. Several unrelated origins in O3O_ONLINE_FRAME_ANCESTORS: coming soon.