Handle errors and retries¶
This page shows how to surface and recover from the failure modes you will encounter when using mde-client in production: authentication failures, HTTP errors from the Defender API (including rate limiting), and empty or failed export blobs.
Failure modes at a glance¶
| Error | Raised from | Typical cause |
|---|---|---|
mde_client.AuthenticationError |
MSALAuth.token |
Bad tenant/client/secret, missing app permissions |
httpx.HTTPStatusError (429) |
Endpoint _request() calls |
Defender API rate limiting |
httpx.HTTPStatusError (4xx / 5xx) |
Endpoint _request() calls |
Permission denied, not found, server-side failures |
httpx.RequestError (subclasses) |
The injected httpx.Client |
DNS, TLS, connection, or timeout failures |
mde_client.EmptyExportBlobError |
ViaFiles internals |
Export URL returned 200 OK with no records |
RuntimeError from ViaFiles |
After exhausted retries | Hard download failure on at least one export blob |
Catch authentication failures separately¶
from mde_client import MDEClient, AuthenticationError
try:
with MDEClient(tenant_id, client_id, client_secret) as client:
rows = client.machines.get_all().to_dicts()
except AuthenticationError as e:
# Treat as configuration error: do not retry.
log.error("MSAL token acquisition failed: %s", e)
raise
AuthenticationError indicates a configuration problem (bad credentials, wrong tenant, missing app permissions). Retrying will not help.
Catch HTTP errors from the Defender API¶
Endpoint methods raise from httpx directly. Inspect the response on the exception:
import httpx
from mde_client import MDEClient
with MDEClient(...) as client:
try:
results = client.machines.get_all()
rows = results.to_dicts()
except httpx.HTTPStatusError as exc:
status = exc.response.status_code
log.warning("Defender API returned %s: %s", status, exc.response.text[:500])
raise
Rate limiting (429 Too Many Requests)¶
Defender returns 429 when you exceed the API rate budget. Inspect the Retry-After header and back off:
import time
import httpx
def call_with_retry(fn, *, attempts: int = 5):
for attempt in range(attempts):
try:
return fn()
except httpx.HTTPStatusError as exc:
if exc.response.status_code != 429 or attempt == attempts - 1:
raise
wait = int(exc.response.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
raise RuntimeError("unreachable")
For long-running batch workloads, prefer to wire retry policy into the injected httpx.Client via httpx-retries or a similar transport-level retry layer rather than wrapping every call site.
Permanent client errors (4xx)¶
401 / 403 indicate the app registration lacks the required permission or admin consent was not granted; do not retry. 404 usually means the resource ID does not exist; treat as an empty result if that is acceptable for your workflow.
Handle failed exports¶
Export-backed endpoints use ViaFiles internally. Transient download failures are retried per ViaFilesConfig; permanent failures surface as RuntimeError after retries are exhausted. Truly empty blobs are recognised via EmptyExportBlobError and skipped — you do not need to catch that exception yourself unless you are calling ViaFiles directly.
try:
rows = client.browser_extension.get_all_files().to_arrow()
except RuntimeError as exc:
# All retries exhausted on at least one blob.
log.error("Export download failed: %s", exc)
raise
Logging and diagnostics¶
mde-client uses module-level loggers as log = logging.getLogger(__name__). To see request- and retry-level detail, enable DEBUG on the package and its dependencies:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("mde_client").setLevel(logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.INFO)
logging.getLogger("msal").setLevel(logging.INFO)