Skip to content

Log why Entra or Graph refused, not just that they did - #2

Open
ryanwjackson wants to merge 1 commit into
mainfrom
fix/specific-graph-error-detail
Open

Log why Entra or Graph refused, not just that they did#2
ryanwjackson wants to merge 1 commit into
mainfrom
fix/specific-graph-error-detail

Conversation

@ryanwjackson

@ryanwjackson ryanwjackson commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Why

A production job failed with exit code 65 twice and then hit BackoffLimitExceeded. The only diagnostic in the log was:

runtime_error=entra_rejected_the_client_id_or_secret

That one label covers an expired secret, a wrong secret, an app that isn't in the tenant, and a wrong tenant id — four different fixes. The site-lookup label is worse: it asserts that Sites.Selected consent or the site grant is missing, when an itemNotFound on a mistyped DEST_SITE_URL produces exactly the same message.

The detail was already there and being discarded. Every call uses curl --fail-with-body, so Microsoft's JSON error body lands in $token_response / $site_response / $drives_response — and then the || runtime_fail ... branch drops it on the floor.

What changed

report_error_detail runs before each runtime_fail and writes two lines to stderr:

entra_curl_exit=22
entra_detail=invalid_client: AADSTS7000222: The provided client secret keys for app ... are expired.  Trace ID: ...
  • Reads .error.code / .error.message (Graph) or .error / .error_description (Entra) — the two response shapes differ, so the jq branches on which one it got.
  • Collapses CRLF to spaces. Entra descriptions are multi-line with a trace id; without this each failure becomes three rows in Log Analytics.
  • Truncates to 800 characters.
  • If the body isn't parseable JSON, logs it raw. An HTML error page means a proxy answered instead of Microsoft, which is worth seeing rather than silently degrading to the label.
  • Emits curl's exit status, so 28 (timeout) or 6 (DNS) is no longer read as a permissions problem. 22 means the service answered with an HTTP error and the detail line holds the reason.

What did not change

Exit codes. 64 is still configuration, 65 still runtime — those are the machine-readable contract and the detail belongs next to them, not in them. No error labels were renamed.

Safety

Bodies are safe to log: neither Entra nor Graph echoes the client secret or the bearer token in an error response. There's a regression test asserting the secret does not appear in the output.

Testing

  • sh tests/transfer_test.sh — passes. Three new cases: the AADSTS code survives on one line and without the secret beside it; a Graph accessDenied reports its code; an unparseable HTML body falls through to raw text.
  • shellcheck app/transfer.sh tests/transfer_test.sh — clean.
  • tests/{config,template,uidefinition}_test.py — pass.

The repo has no CI workflows, so the above was run locally.

Note for @prazgaitis

This does nothing for runs that have already failed — it only helps once a new image ships. For an existing failure the reason still has to come from the console table:

ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "<job-name>"
| where Log_s has "runtime_error" or Log_s has "configuration_error"
| project TimeGenerated, Log_s
| order by TimeGenerated desc

Separately, and not addressed here: a deployed job can be configured with a cron expression such as 0 0 31 2 *, which logs InvalidSchedule. February 31st never occurs, so that trigger never fires. Worth validating at deploy time.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HPhMRKvjYDL7xtEesucLQU

Every failing call already used curl --fail-with-body, so Microsoft's
error body was captured into the response variable and then thrown away
by the `|| runtime_fail` branch. The job log carried only a hand-written
label, which collapses four different fixes into one message: an expired
secret, a wrong secret, an app missing from the tenant and a wrong tenant
id all surfaced as entra_rejected_the_client_id_or_secret, and a missing
site grant was indistinguishable from a wrong DEST_SITE_URL.

Report the body alongside the label. Entra's AADSTS code and Graph's
error.code are the actionable part, and neither service echoes the client
secret or the bearer token in an error response. curl's own exit status
goes out too, so a timeout or a DNS failure is no longer read as a
permissions problem.

The exit codes are unchanged: 65 stays the machine-readable signal and
the detail sits beside it in the console log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPhMRKvjYDL7xtEesucLQU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants