Skip to content

fix(azure): cap error details by characters so non-ASCII bodies don't panic - #805

Merged
martintrojer merged 1 commit into
agavra:mainfrom
MaxFreedomPollard:fix/azure-error-detail-char-boundary
Oct 26, 2026
Merged

martintrojer merged 1 commit into
agavra:mainfrom
MaxFreedomPollard:fix/azure-error-detail-char-boundary

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Problem

When an Azure DevOps request fails with a long error body, tuicr can panic instead of showing the error. map_http_error passes every failed response body (and az rest stderr) through trim_detail, and that function capped the text with a byte slice:

if collapsed.len() > 400 {
    format!("{}…", &collapsed[..400])

If byte 400 falls inside a multi-byte character, &collapsed[..400] panics with "byte index 400 is not a char boundary". Error bodies are not guaranteed to be ASCII: they can quote repository or user names, or be HTML error pages.

  • With tuicr pr <N>, the request runs on the main thread, so the panic ends tuicr.
  • Opening, reloading or submitting a PR from inside the TUI runs the request on a background thread. There the panic hook restores the terminal while the UI is still running, and the request never reports back. For example, a PR open just keeps spinning.

Cause

trim_detail in src/forge/azure/az.rs measures and slices in bytes. The Gerrit backend has its own copy of this helper in src/forge/gerrit/api.rs, and that copy caps by characters for exactly this reason. Its regression test is should_cap_error_detail_without_splitting_a_multi_byte_character. The Azure copy is older and was never updated.

Fix

Use the Gerrit implementation as is: cut at char_indices().nth(MAX_DETAIL_CHARS), with the 400 limit now named MAX_DETAIL_CHARS. ASCII bodies are trimmed exactly as before (same length, same …). For non-ASCII text, the only change is that the cut now lands on a character boundary.

Tests

  • trim_detail_caps_characters_without_splitting_a_multi_byte_character is a copy of the Gerrit test. The input is 399 a followed by ten €, so byte 400 falls inside the first €. The old code panics on it. The new code keeps 400 characters and adds ….
  • trim_detail_leaves_short_detail_untrimmed_and_collapses_whitespace pins the existing whitespace collapsing.

I don't have a Rust toolchain where I prepared this, so I have not compiled it or run cargo test, cargo fmt or cargo clippy on this branch. CI will be the first build. The function body is byte-for-byte the same as the Gerrit one that is already on main. I also checked the old and new logic with a small Python model of Rust's string slicing:

  • The test input panics before the change and passes after it.
  • ASCII bodies of 390 to 410 characters give identical output before and after.

🤖 Generated with Claude Code

… panic

`trim_detail` capped an Azure DevOps error body with `&collapsed[..400]`,
a byte slice. When byte 400 fell inside a multi-byte character, the
slice panicked instead of producing the error message. Error bodies are
not guaranteed to be ASCII: they can quote repository or user names, or
be HTML error pages.

The Gerrit backend's copy of this helper already caps by characters for
exactly this reason. Use the same implementation here, with the same
400-character limit named as `MAX_DETAIL_CHARS`. ASCII bodies are
trimmed exactly as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@martintrojer

Copy link
Copy Markdown
Collaborator

Thanks!

@martintrojer
martintrojer merged commit e796824 into agavra:main Oct 26, 2026
5 checks passed
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