Forum Discussion
Classic Outlook event-based add-in: fetch/XMLHttpRequest fails in JavaScript-only runtime
We are testing outbound HTTP from an Outlook add-in event-based activation handler running in the classic Outlook for Windows JavaScript-only runtime used by:
<Override type="javascript" resid="JSRuntime.Url"/>
Question
Is response-bearing HTTP via fetch or XMLHttpRequest officially supported from the classic Outlook for Windows JavaScript-only event runtime?
If it is supported:
- Are there additional manifest, policy, trust, or runtime requirements beyond:
- absolute HTTPS URLs,
- <AppDomains>,
- .well-known/microsoft-officeaddins-allowed.json,
- Office.actions.associate(...),
- and using a single bundled JS file?
If it is not supported or unreliable in this runtime:
- What is the recommended/supported pattern for an OnAppointmentSend handler that requires a server-side validation decision before allowing send?
We are currently considering this workaround architecture:
- Precompute the validation result in the taskpane/WebView runtime.
- Store the decision in Outlook custom properties.
- Read the stored value from events.js during OnAppointmentSend.
Is this workaround required, or should direct outbound HTTP from the event runtime work reliably?
Environment
Client tested:
- Microsoft Outlook 2016 MSO
- Version 2604
- Build 16.0.19929.20172
- 32-bit
Platform logged by Office.js:
Office.context.platform === "PC"
The launch event itself works correctly:
- OnAppointmentSendHandler is invoked.
- Office.actions.associate(...) works.
- event.completed(...) is delivered to Outlook successfully.
The issue is specifically that response-bearing HTTP from events.js does not complete reliably.
Relevant manifest excerpt
<VersionOverrides
xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1"
xsi:type="VersionOverridesV1_1">
<Requirements>
<bt:Sets DefaultMinVersion="1.12">
<bt:Set Name="Mailbox" />
</bt:Sets>
</Requirements>
<Hosts>
<Host xsi:type="MailHost">
<Runtimes>
<Runtime resid="WebViewRuntime.Url" lifetime="short">
<Override type="javascript" resid="JSRuntime.Url" />
</Runtime>
</Runtimes>
<DesktopFormFactor>
<ExtensionPoint xsi:type="LaunchEvent">
<LaunchEvents>
<LaunchEvent
Type="OnAppointmentSend"
FunctionName="OnAppointmentSendHandler"
SendMode="SoftBlock" />
</LaunchEvents>
<SourceLocation resid="WebViewRuntime.Url" />
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Urls>
<bt:Url
id="WebViewRuntime.Url"
DefaultValue="https://example-addin-host.example.com" />
<bt:Url
id="JSRuntime.Url"
DefaultValue="https://example-addin-host.example.com/events.js?v=2.0.0.16" />
</bt:Urls>
</Resources>
</VersionOverrides>
Well-known event runtime CORS configuration
We also configured the documented well-known URI for event runtime CORS:
{ "allowed": [ "https://example-addin-host.example.com/events.js?v=2.0.0.16" ] }
Hosted at:
https://example-addin-host.example.com/.well-known/microsoft-officeaddins-allowed.json
Minimal events.js repro
(function (root) {
function OnAppointmentSendHandler(event) {
console.log('[test] handler triggered', {
hasFetch: typeof fetch,
hasXMLHttpRequest: typeof XMLHttpRequest,
platform: Office.context.platform
});
var fetchStartedAt = Date.now();
setTimeout(function () {
console.log('[test] fetch still pending', {
elapsedMs: Date.now() - fetchStartedAt
});
}, 4500);
fetch('https://example-addin-host.example.com/manifest.xml', {
method: 'GET',
cache: 'no-store'
}).then(
function (response) {
console.log('[test] fetch response', {
elapsedMs: Date.now() - fetchStartedAt,
ok: response.ok,
status: response.status,
statusText: response.statusText,
url: response.url
});
return response.text();
},
function (error) {
console.log('[test] fetch error', {
elapsedMs: Date.now() - fetchStartedAt,
name: error && error.name,
message: error && error.message
});
}
);
var xhrStartedAt = Date.now();
var xhr = new XMLHttpRequest();
xhr.timeout = 4500;
xhr.onreadystatechange = function () {
console.log('[test] xhr readyState', {
elapsedMs: Date.now() - xhrStartedAt,
readyState: xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
responseURL: xhr.responseURL,
responseTextLength: xhr.responseText ? xhr.responseText.length : 0
});
};
xhr.onload = function () {
console.log('[test] xhr load', {
elapsedMs: Date.now() - xhrStartedAt,
status: xhr.status,
responseTextLength: xhr.responseText ? xhr.responseText.length : 0
});
};
xhr.onerror = function () {
console.log('[test] xhr error', {
elapsedMs: Date.now() - xhrStartedAt,
status: xhr.status
});
};
xhr.ontimeout = function () {
console.log('[test] xhr timeout', {
elapsedMs: Date.now() - xhrStartedAt,
readyState: xhr.readyState,
status: xhr.status
});
};
xhr.open(
'GET',
'https://example-addin-host.example.com/manifest.xml',
true
);
xhr.send();
setTimeout(function () {
console.log('[test] completing event after wait');
event.completed({ allowEvent: true });
}, 6500);
}
root.OnAppointmentSendHandler = OnAppointmentSendHandler;
Office.actions.associate(
'OnAppointmentSendHandler',
OnAppointmentSendHandler
);
})(this);
Observed behavior
- The handler logs successfully.
- typeof fetch === "function".
- typeof XMLHttpRequest === "function".
However:
- fetch(...) starts, but no response callback is logged.
- In earlier tests, fetch remained pending until our timeout marker.
- XMLHttpRequest reaches readyState: 1 after open(), but does not return useful response data.
- In some runs it eventually reports:
- status: 0,
- empty response,
- or timeout.
- Server-side logs show the request does not reach the server in the failing cases.
- event.completed(...) works and Outlook receives the completion.
Things already ruled out / tested
- We are using absolute URLs, not relative URLs.
- We tested both:
- https://localhost:3000/...
- http://localhost:3000/...
- We tested through an HTTPS tunnel pointing to localhost.
- We tested trusted local HTTPS certificates; this does not appear to be a certificate trust issue.
- We configured .well-known/microsoft-officeaddins-allowed.json as documented for event-based CORS.
- We added the target domains to <AppDomains>.
- We tested public safe domains such as JSONPlaceholder-style endpoints.
- This does not appear to be ordinary CORS because in failing cases the request never reaches the server.
- The events.js bundle is intentionally simple and compatible with older JavaScript syntax.
- We avoided modern syntax and can provide an ES5/ES2015 version if needed.
- The runtime itself loads and executes the handler correctly.