Files
Victor Lyuboslavsky dc3694102f Updating SSE to be spec compliant, which now appears to work with ngrok (#45988)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #45862 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
  - Already added in the previous PR.

## Testing

- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved detection and reporting of enrollment errors sent by the
server, ensuring error messages surface reliably.
* Prevented streaming leaks by stopping background work when a client
disconnects.

* **Documentation / Protocol**
* Made server-to-client streaming more spec-compliant (framing,
heartbeats) for more robust Android Enterprise enrollment communication.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45988?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-22 12:21:43 -05:00

96 lines
3.3 KiB
TypeScript

import sendRequest from "services";
import endpoints from "utilities/endpoints";
import authToken from "utilities/auth_token";
interface IGetAndroidSignupUrlResponse {
android_enterprise_signup_url: string;
}
interface IGetAndroidEnterpriseResponse {
android_enterprise_id: boolean;
}
export default {
getSignupUrl: (): Promise<IGetAndroidSignupUrlResponse> => {
const { MDM_ANDROID_SIGNUP_URL } = endpoints;
return sendRequest("GET", MDM_ANDROID_SIGNUP_URL);
},
getAndroidEnterprise: (): Promise<IGetAndroidEnterpriseResponse> => {
const { MDM_ANDROID_ENTERPRISE } = endpoints;
return sendRequest("GET", MDM_ANDROID_ENTERPRISE);
},
turnOffAndroidMdm: (): Promise<void> => {
const { MDM_ANDROID_ENTERPRISE } = endpoints;
return sendRequest("DELETE", MDM_ANDROID_ENTERPRISE);
},
/**
* This function starts a Server-Sent Events connection with the fleet server
* to get messages about a successful Android mdm connection. We have to use
* fetch here because the EventSource API does not support setting headers,
* which we need to authenticate the request.
*/
startSSE: (abortSignal: AbortSignal): Promise<void> => {
return new Promise(async (resolve, reject) => {
try {
const response = await fetch(endpoints.MDM_ANDROID_SSE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken.get()}`,
Accept: "text/event-stream",
},
signal: abortSignal,
});
const reader = response?.body?.getReader();
if (!reader) {
reject(new Error("Android MDM SSE stream unavailable"));
return;
}
const decoder = new TextDecoder();
const successSignal = "data: Android Enterprise successfully connected";
const errorMarker = "event: error";
// Buffer accumulates decoded text so a frame split across multiple
// chunks (valid with chunked transfer encoding) is still detected.
const tailLen = Math.max(successSignal.length, errorMarker.length);
let buffer = "";
let loggedError = false;
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await reader.read();
if (done) {
// Server closed the stream without ever sending success.
// Reject so callers don't await forever on unmount or backend hiccup.
reject(new Error("Android MDM SSE ended before success signal"));
return;
}
buffer += decoder.decode(value, { stream: true });
// Surface backend-emitted error events. Chrome's EventStream tab
// doesn't attach to fetch-based SSE reliably, so without this log
// the only signal of a server-side failure is the generic UI
// toast.
if (!loggedError && buffer.includes(errorMarker)) {
console.error("[android-sse]:", buffer);
loggedError = true;
}
if (buffer.includes(successSignal)) {
resolve();
return;
}
buffer = buffer.slice(-tailLen);
}
} catch (error) {
if ((error as Error).name === "AbortError") {
// we want to ignore abort errors
console.error("SSE Fetch aborted");
} else {
reject(error);
}
}
});
},
};